Wzorzec Model View Controller w aplikacjach na iOS stosuję już od jakiegoś czasu, jednak gdzieś z tyłu głowy coś podpowiadało mi, że chyba nie do końca wszystko robię jak należy. Właśnie rozpocząłem prace nad stosunkowo niewielką aplikacją pisaną specjalnie dla mojego obecnego pracodawcy, więc pomyślałem sobie, że to bardzo dobra okazja żeby sprowadzić wszystko jeszcze raz do podstaw i sprawdzić gdzie popełniam błędy. Oczywiście jest raczej mało prawdopodobne, że uda mi się wszystkie wyłapać 😎.

Zacząć musimy od tego, że wzorzec MVC jest zalecany przez Apple dla tworzenia struktury aplikacji dla iOS. Pomimo tego zapewne wielu programistów, którzy mają już kilka linijek kodu za sobą, przyzna się do tego, że aspekt architektury był prawie całkowicie pomijany w ich pierwszych aplikacjach. Ze mną było podobnie, chociaż ja zaczynałem od pisania gier w Unity3D.

Bardzo szybko okazało się jednak, że brak odpowiedniej architektury na dłuższą metę niemal całkowicie uniemożliwia tworzenie rozbudowanych i stosunkowo łatwych w testowaniu aplikacji. Piszę stosunkowo, ponieważ zagadnienie związane z testami jednostkowymi zawsze w mojej ocenie będzie sprawiło pewne trudności 😋.

 

 

Jak sama nazwa wskazuje MVC składa się z trzech komponentów:

Controller – element, który łączy warstwę Model oraz warstwę View. Controller nie powinien znać konkretnej implementacji Widoku (View), z którym będzie współpracował. Zamiast tego powinien komunikować się z jego abstrakcją za pomocą interfejsu.

Model – warstwa, w której umieszczamy dane. W tej warstwie będziemy umieszczali nasze modele, kod odpowiedzialny za współpracę z bazą danych lub zdalnym serwerem.

View – warstwa, którą można traktować jako front naszej aplikacji. Założenie jest takie, że powinna ona być tak bierna jak to tylko możliwe. Przykładem obiektu należącego do tej warstwy jest choćby każdy obiekt dziedziczący po UIView.

Tak się to prezentuje na grafice:

 

Źródło: Dokumentacja Apple

 

Oczywiście od strony teoretycznej wszystko wydaje się być w miarę proste, ale na własnej skórze doświadczyłem, że problem pojawia się w momencie kiedy zaczniemy zastanawiać się w jakiej warstwie konkretny obiekt umieścić.

 

Warstwa Controller

 

To w tej warstwie powinny pojawiać się wszelkiego rodzaju odwołania do serwerów, lokalnych baz danych, UserDefaults itp.. W niej także powinien znajdować się kod, który będzie decydował (w oparciu o pozyskane dane) jakiego rodzaju działania należy podjąć. W środowisku programistów iOS pojawia kilka interpretacji tego jakie dokładnie obiekty należą do tej warstwy. Niektórzy uważają, że obiekt dziedziczący po klasie UIViewController jest bardzo dobrym kandydatem na obiekt obsługujący logikę biznesową naszej aplikacji, jednak z mojego skromnego doświadczenia wynika, że UIViewController powinien jedynie pośredniczyć pomiędzy warstwą logiki a warstwą UI. Jak dokładnie to osiągnąć będzie za chwilę 😎.

Jeżeli zdecydujemy się na umieszczanie wszystkich elementów interfejsu użytkownika oraz logiki biznesowej w podstawowym obiekcie UIViewController to bardzo szybko doprowadzimy do sytuacji, w której stanie się on niepotrzebnie rozbudowany (syndrom Massive View Controller), a przede wszystkim bardzo trudny do przetestowania. W ten sposób zaprzepaścimy szansę, którą daje nam MVC – tworzenia aplikacji w oparciu o niezależne, łatwe w testowaniu moduły.

Tak wiem, bez konkretnych przykładów powyższy opis może wydawać się zbyt abstrakcyjny lub zwyczajnie niezrozumiały 😓. Spokojnie, opiszę jeszcze tylko warstwę View oraz Model i będzie konkretny przykład 😉.

 

Warstwa View

 

W tej warstwie znajdują się wszystkie elementy interfejsu użytkownika. To tutaj powinny zostać umieszczone wszystkie obiekty dziedziczące po UIView oraz elementy bibliotek takich jak UIKit, Core Animation, czy też Core Graphics.

To tutaj również, w mojej opinii, powinien zostać umieszczony obiekt pochodny UIViewController, którego przeznaczenie w całej aplikacji jest dość szczególne, o czym napisałem już powyżej. Podobnie jak w przypadku wzorca Model View Presenter, znanego głównie z Androida, warstwa View powinna być traktowana jako ta „najgłupsza” część całej układanki. W idealnej sytuacji warstwa ta nie powinna zawierać żadnej logiki biznesowej, a jedynie kod wymagany do zaprezentowania danych na ekranie. View Layer jest również odpowiedzialna za interpretację interakcji użytkownika z interfejsem oraz przekazanie uzyskanych danych do bardziej kompetentnych obiektów 😎. Konkretny przykład – kiedy użytkownik naciśnie jakiś przycisk na ekranie to warstwa View powinna tę informację odebrać i przekazać dalej do kontrolera, aby mógł podjąć na jej podstawie określone działania. Warstwa View nie powinna zajmować się bezpośrednią interakcją z warstwą Model, ani też zawierać logiki biznesowej (będzie można wykorzystać dany widok ponownie w innym miejscu w aplikacji). Do tego też za chwilę będzie przykład, don’t panic 😒.

 

Warstwa Model

 

Tutaj pełne zaskoczenie – znajdować się w niej będą wszystkie nasze modele 😃. Prosty przykład:

 

 

 

Jednak w warstwie tej będziemy również umieszczać kod odpowiedzialny za komunikację z lokalną bazą danych / serwerem / innymi zdalnymi usługami (wszelkiego rodzaju serwisy i managery 😆😆😆, jak również kod odpowiedzialny z parsowanie oraz mapowanie danych.

 

Jak to wszystko poskładać?

 

Teraz chyba część najważniejsza. Jak to wszystko wykorzystać w praktyce. Zaznaczam od razu (info głównie dla początkujących), że jest to moja wizja implementacji tego wzorca stworzona w oparciu o moje skromne doświadczenie. Bez problemu znajdą się pewnie osoby, którym takie rozwiązanie nie będzie odpowiadało. Jest to rzecz zupełnie naturalna i wręcz wymagana, ponieważ tylko dzięki różnicy zdań i otwartej dyskusji możliwy jest postęp. Nie istnieje też coś takiego jak jedyna słuszna implementacja wzorca MVC. Wszystko uzależnione jest od projektu, w którym zostanie on wykorzystany, a także preferencji zespołu (lub pojedynczej osoby) zajmującego się jego implementacją.

 

Warstwa Controller w praktyce

 

W obiekcie, który będzie odpowiadał za sterowanie logiką biznesową, na samy początku zawsze tworzę protokoły, które będą określały jakie zakres odpowiedzialności będzie miała ta warstwa i jak powinna reagować na to warstwa View. Wszystko umieszczam w jednym pliku dla zwiększenia czytelności. Tak jest też łatwiej w trakcie rozbudowywania aplikacji – mamy wszystko pod ręką i nie trzeba skakać pomiędzy plikami. Tak to będzie wyglądało:

 

 

Powyżej zdefiniowaliśmy dwa protokoły, które określają jakie podstawowe funkcjonalności powinien spełniać kontroler odpowiedzialny za logikę oraz widok, który będzie wyświetlał szczegóły o kolekcji ptaków. Dzięki temu, że nasze implementacje będziemy opierać na protokołach, będziemy mieli możliwość „wstrzykiwania” zależności do naszych obiektów, co bardzo ułatwi nam testowanie poszczególnych elementów (za chwilę pokażę o co chodzi). Wykorzystanie protokołów pomaga również w tworzeniu niezależnych od siebie modułów. W razie konieczności wystarczy jedynie podmienić daną implementację.

Teraz dodajmy konkretną implementację obiektu wykorzystującego protokół BirdsControllerContract. Jak już wspominałem, wszystko umieszczamy w jednym pliku:

 

 

Implementacja ta korzysta z WebService, który za chwilę dodamy. W konstruktorze obiektu BirdsController umieściłem wymóg podania obiektu typu WebService, z którego BirdsController będzie korzystał. Tutaj również oprzemy się na protokole, a nie na konkretnym obiekcie. W tym miejscu widać najlepiej zalety „wstrzykiwania” zależności (dependency injection). Jeżeli będziemy chcieli napisać testy dla naszego kontrolera, to podczas jego tworzenia będziemy mogli z łatwością podać mu obiekt, który będzie tylko symulował (mock) połączenie z serwerem, dzięki czemu będziemy mogli przeprowadzić testy w izolacji, bez polegania na kaprysach serwera lub połączenia z siecią.

 

Trochę więcej o mockach i testowaniu ogólnie możecie poczytać w tym miejscu.

 

W zależności od wyniku połączenia z serwerem (będziemy je oczywiście tylko symulować), mogą zostać podjęte dwie akcje obsługiwane przez nasz widok (którego jeszcze nie dodaliśmy) – zaktualizowanie wyświetlanej kolekcji lub wyświetlenie wiadomości o błędzie. Wykorzystujemy w tym przypadku Delegate Pattern ponieważ pozwala on na nawiązanie komunikacji pomiędzy dwoma obiektami bez tworzenia silnych zależności. W przypadku naszego widoku ponownie opieramy się na protokole, zamiast na konkretnej implementacji.

Dodajmy teraz protokół oraz jego implementację, która będzie symulowała połączenia z serwerem:

 

 

Dla ułatwienia testowania parametr showError w funkcji getAllBirds() będzie służył do określenia, czy chcemy wywołać błąd, czy też finalnie przekazać kolekcję składającą się z dwóch ptaków. Dla pełnego obrazu umieszczam jeszcze wykorzystany model:

 

 

Już prawie jesteśmy na miejscu 😄. Jeszcze tylko przykładowa klasa dla naszego widoku i będzie to można to wszystko sklejać:

 

 

Obiekt ten ma tylko zaprezentować koncepcję, dlatego nie umieszczam w nim implementacji Collection View, czy też jakiegoś okna z errorem, ponieważ nie jest to istotne dla naszego przykładu.

A teraz zobaczcie jak to wszystko poskładać w całość za pomocą obiektu View Controller:

 

 

Podczas tworzenia obiektu BirdsCollectionVC możemy skorzystać z dependency injection, co ułatwi nam testowanie. W funkcji viewDidLoad() tworzymy obiekt, który będzie odpowiadał za wyświetlanie danych o ptakach, ustawiamy go jako delegata dla naszego kontrolera, a następnie pobieramy z serwera naszą kolekcję (dla fikcyjnego użytkownika o id równym 1). Po wykonaniu tej operacji w obiekcie BirdsView zostaną wywołane odpowiednie metody, zgodnie z tym co zdefiniowaliśmy w obiekcie BirdsController.

Zwróćcie uwagę jak czytelny stał się teraz kod (a przynajmniej będzie taki jak już się we wszystkim połapiemy 😄😄😄). Obiekt View Controller nie jest zapchany ani kodem odpowiedzialnym za komunikację z serwerem, ani też kodem odpowiedzialnym za prezentowanie danych. Wszystkie te zadania zostały przejęte przez inne obiekty. Jeszcze raz podkreślę, że takie podejście ułatwia pisane testów jednostkowych. Zamiast pisać je dla jednego, złożonego i skomplikowanego obiektu, możemy zrobić to dla kilku mniejszych, bardziej czytelnych. Dzięki temu możemy tworzyć aplikacje w oparciu o moduły o wysokim stopniu separacji, czyli niewielkim, wzajemnym sprzężeniu.

Nie jest to jednak rozwiązanie idealne, ponieważ podczas ładowania widoku korzystamy z konkretnej implementacji dla BirdView, co może utrudnić nam pisanie testów dla BirdsCollectionVC, ale w którymś miejscu obiekt BirdView musi zostać utworzony.

Zdaję sobie sprawę, że przykład ten jest bardzo prosty i nie oddaje w pełni sytuacji z jakimi spotykamy się na co dzień podczas pisania rozbudowanych aplikacji komercyjnych. Pokazuje on jednak jeden ze sposobów w jaki można radzić sobie z przeciążonymi obiektami View Controllers, które są zmorą programistów iOS, ale również Androida, choć pod trochę inną postacią.

Za jakiś czas na podstawie powyższego przykładu pokażę jak można pisać testy poszczególnych modułów 😎.

 


 

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *