MVC vs. MVVM. Dlaczego młodszy brat zyskuje na popularności?

Przed rozpoczęciem części zasadniczej artykułu chciałbym na wstępie zaprosić Was na cykl, który rozpoczyna ten tekst. Poświęcę w nim trochę czasu na zaznajomienie Was z kilkoma dobrymi praktykami, które według mnie warto stosować podczas developowania aplikacji mobilnych (i nie tylko mobilnych) oraz przydatnymi bibliotekami wartymi uwagi, gdy zabieramy się za programowanie mobilków pod systemem z Cupertino.

Adam Włodarczyk. Właściciel firmy Hindbrain tworzącej oprogramowanie na urządzenia mobilne oraz systemy CRM w różnych technologiach. Jego ulubionymi językami są Java i Objective-C, chociaż próbuje też sił w językach takich jak Kotlin czy Swift. Zdarza się, że nawet czasem pokoduje w PHP’ie. Żeby dać ujście ekstrawertycznej części swojej osobowości, uczy i jest mentorem kolejnego pokolenia programistów w Coderslab. Po godzinach stara się dbać o kręgosłup pływając i ćwicząc na siłowni.


Wprowadzenie

Rozpocznijmy zatem od podstaw, każdy programista powinien znać pojęcie wzorców projektowych. Są one bardzo pomocne podczas tworzenia rozwiązań dla klientów, przyspieszają znacząco pracę, gdyż podążamy przez wcześniej utarte i ugruntowane w wielu projektach ścieżki (zmniejszamy tym samym prawdopodobieństwo popełnienia błędu i porażki). Wzorce pozwalają także koleżankom i kolegom po fachu zrozumieć tok myślenia oraz koncepcje jakie staraliśmy się upakować w danej technologii. Teraz trochę faktografii dla zaznajomienia tych, którzy może rozpoczynają swoją przygodę z programowaniem lub w ogóle są zieloni w temacie wzorców projektowych, czyli czym te wzorce są, jak się je grupuje i gdzie szukać informacji o nich.

Wzorce projektowe (ang. design patterns) jak sama nazwa wskazuje to ugruntowane, uniwersalne, sprawdzone w wielu przypadkach i środowiskach, przetestowane schematy postępowania podczas rozwiązywania różnych problemów projektowych. Przedstawiają sposób powiązania i zależności między klasami oraz obiektami, a także ułatwia tworzenie, zarządzanie i utrzymywanie kodu źródłowego. Wzorce projektowe są opisem rozwiązania, czyli takim cook bookiem, a nie implementacją.

Podstawowy podział jaki wykorzystywany jest do ustrukturyzowania wzorców projektowych to podział na trzy kategorie:

  • Kreacyjne opisują w jaki sposób obiekty są tworzone;
  • Strukturalne opisują struktury powiązanych ze sobą obiektów;
  • Behawioralneopisują zachowania i odpowiedzialności współpracujących ze sobą obiektów.

W dzisiejszym materiale skupimy się na wzorcach strukturalnych, które zawarte zostały w tytule tego artykułu, czyli MVC i MVVM. Zarówno jeden, jak i drugi wzorzec wykorzystywany jest do podziału aplikacji na spójnie logicznie elementy zgodnie z pewnymi normami i zasadami jakie zostały przyjęte w tych podejściach.

Wzorzec MVC

MVC, czyli Model-View-Controller jest wzorcem, który dzieli aplikacje na trzy warstwy. Pierwszą jest model danych wraz z logiką biznesową oraz dostępem do danych. W tej warstwie następuje wymiana danych, ich mutacje, filtrowanie. Następną warstwą jest widok, czyli warstwa prezentacji danych oraz interakcji z użytkownikiem. Ostatnia warstwa to kontroler, który zarządza połączeniem widoku z modelem oraz innymi elementami wykorzystywanymi w aplikacji (sesją, serwer api, powiadomienia itd.). Ten sposób tworzenia kodu wykorzystywany jest podczas programowania aplikacji mobilnych na urządzenia spod znaku nadgryzionego jabłka (do dziś). Ma to jednak swoje konsekwencje, gdyż kod źródłowy (szczególnie w kontrolerze) staje się mało czytelny. Agreguje w sobie połączenie modelu z widokiem, dodatkowo posiada często metody do pobierania czy wysyłania danych do serwera RESTowego, zarządza powiadomieniami, nasłuchuje zdarzeń, generalnie robi bardzo dużo.

Podczas tworzenia aplikacji zgodnie z tym wzorcem można dojść do pewnej wprawy, która przeradza się czasem w ekwilibrystykę skakania między różnymi klasami w celu podzielenia kodu z kontrolera na mniejsze elementy, ale chyba nie chodzi nam w programowaniu o utrudnianie sobie życia tylko o jego ułatwianie.

Rysunek 1. Wzorzec MVC

Wzorzec MVVM

Z pomocą przyszedł kolejny wzorzec strukturalny, czyli MVVM. Rozwinięcie skrótu oznacza ModelView-View-Model. Jest to wzorzec, który stał się niejako rozwinięciem wzorca MVC i implementuje w sobie zasady clean architecture, czyli odseparowania od siebie elementów tak, aby kod był jak najbardziej niezależny od platformy. Dobra, czyli co ten wzorzec tak naprawdę robi? Tak naprawdę oprócz standardowego widoku (warstwy prezentacji, która w tym wypadku jest trochę połączeniem prezentera z kontrolerem) oraz modelu, który odpowiada nam za wymianę danych oraz enkapsuluje nam je w odpowiedniej formie, mamy również nowy byt pt. ViewModel.

Jest to połączenie warstwy modelowej z elementem przejściowym, który z danych wyciąga nam jedynie niezbędne dla widoku informacje, zawiera metody konieczne do prawidłowego wyfiltrowania czy zmapowania danych oraz zarządza logiką biznesową. Jest powoływany do życia wtedy, gdy warstwa prezentacji będzie gotowa i odseparowuje od niej logikę biznesową. Wzorzec ten pozwala zachować przepływ zgodny z zasadami czystej architektury (czyli od prezentera do warstwy danych).

Rysunek 2. Clean architecture

Krótko mówiąc dostajemy dodatkową warstwę, która spina dane z warstwą prezentacji, a dodatkowo dostajemy miejsce, gdzie możemy zaimplementować wszystkie metody zarządzające logiką biznesową. Dodatkowo możemy się w niej komunikować z innymi serwisami, co zdecydowanie odchudza warstwę kontrolera. Teraz w niej możemy skupić się na komunikacji z widokiem oraz zarządzaniu akcjami z użytkownikiem.

Rysunek 3. MVVM

Case study

Po takim krótkim i treściwym wstępie czas przejść do jakiegoś przykładu. Przygotowałem małą aplikację na iOS’a (do pobrania tutaj). Jest to bardzo prosta aplikacja pozwalająca zapisywać, odznaczać i usuwać zadania, które przed sobą postawimy. Dodatkowo mamy możliwość zmiany ich priorytetów. Aplikacja posiada w sobie kontroler, który ma widok dodawania zadania oraz listę tych zadań. Lista ma zaimplementowany system sortowania elementów od najnowszego do najstarszego oraz według priorytetów. Zadania oznaczone jako zakończone, są wyświetlane na samym końcu listy.

Aplikacja będzie rozwijana w kolejnych artykułach także zachęcam do lektury.

Pierwszym podejściem jakie przygotowałem dla Was, to wykorzystanie wzorca MVC w aplikacji. Struktura projektu wygląda następująco.

Rysunek 4. Struktura projektu MVC

TodoItem zawiera strukturę danych z jakiej będę korzystał. Nie jest ona jakoś mocno rozwinięta bo też projekt nie jest duży.

Wykorzystałem tu implementacje interfejsu Codable tak, aby dostać automatycznie generowany  konstruktor oraz móc później bez zbędnego problemu skonwertować obiekt do JSONa i z powrotem. Atrybut priority jest zdefiniowany jako enum, który reprezentowany jest poprzez liczbę całkowitą (Int).

Za przechowywanie i wymianę danych odpowiedzialny jest DataManagerService. Jest to klasa typu Singleton, zapisuje dane do lokalnego storage’a (UserDefaults). Żeby uzyskać efekt loose coupling’u, czyli luźnego powiązania między implementacją a wywołaniem, wykorzystuje protokoły, które pozwolą mi bez najmniejszego problemu zastosować wstrzykiwanie zależności (ang. Dependency Injection). Oznacza to, że klasa wykorzystująca dany protokół nie wie, jak zaimplementowana jest logika biznesowa, ale za zna sygnatury metod, więc istnieje możliwość wstrzyknięcia konkretnej implementacji.

Powyższy protokół definiuje zestaw funkcji oraz atrybutów, które będą zdefiniowane w klasie implementacyjnej, a w kontrolerze podczas jego tworzenia będzie wykorzystywana jako pole trzymające implementację, co pozwoli w przyszłości w łatwy sposób zmienić ewentualną implementację na bazę danych lub zewnętrzne RESTful API.

W implementacji serwisu zaszyta jest zmienna toDoItems, która podczas pobierania listy od razu ją sortuje. Najpierw sprawdzane jest czy elementy są oznaczone jako nie zrobione, następnie sprawdzany jest priorytet od najważniejszego do najmniej ważnego, jako ostatnia sprawdzana jest data utworzenia zadania w kolejności malejącej (od najnowszej do najstarszej).

Poniżej przedstawione są funkcje zaimplementowane zgodnie z protokołem interfejsu.

Funkcja storeIntoUserDefault koduje tablicę toDoItems do JSONa, a następnie zapisuje ją pod odpowiednim kluczem w local storage.

Czas na przygotowanie kontrolera. W nim skonfigurowany jest widok ToDoHeaderView oraz UITableView. Do prawidłowej komunikacji wykorzystywane są protokoły dla tych widoków (odpowiednik listenera/handlera w innych językach programowania).

Na wstępie musimy dodać widok, który składa się z inputa i przycisku do dodawania elementu do naszej listy. Jest on dodany jako Outlet w widoku Interface Buildera, także w samym kodzie przetrzymuję jedynie referencję do niego, a także definiuję kto będzie nasłuchiwał zdarzeń. W tym przypadku jest to kontroler właśnie, implementuje on metodę protokołu.

W samym kontrolerze, po jej wywołaniu przez widok, tworzony jest obiekt ToDoItem, który następnie wysyłany jest do dataMangera, zapisywany jest w local storage, a po wykonaniu tych wszystkich operacji, kontroler odświeża widok.

Kontroler sam w sobie musi również zaimplementować wszystkie niezbędne metody do obsługi tableView, czyli:

Jak łatwo zauważyć, nasz kontroler stał się dość mocno rozbudowany, a to tylko dwa komponenty (header i tableView). Co by było gdybyśmy zaimplementowali jeszcze kilka dodatkowych elementów w widoku… spaghetti jak nic.

Warto jeszcze wspomnieć o klasie odpowiadającej za wyświetlanie komórki w tabeli. Przygotowałem prosty widok, który wyświetla tytuł, priorytet oraz datę utworzenia elementu. Dodatkowo w zależności od statusu, tekst jest przekreślony lub nie.

TableViewCell, musi sam zająć się sprawdzeniem wszystkich warunków, dobraniem odpowiednich kolorów, sprawdzeniem statusu i przekonwertowaniem daty na odpowiedni tekst. Trochę dużo pracy jak na prostą komórkę.

Z opanowaniem tego chaosu przychodzi wzorzec MVVM. Przejmuje on kilka odpowiedzialności, którymi musi teraz zajmować się ViewController. Dzięki nowej warstwie (viewModel) znajdującej się pomiędzy prezenterem a modelem danych, jesteśmy w stanie przenieść część (nawet większą) logiki biznesowej i tym samym odciążyć i odchudzić ViewController, co za tym idzie, kod stanie się bardziej modułowy, będzie o wiele bardziej przejrzysty i łatwiejszy w zarządzaniu. Dodatkowo zyskamy warstwę, która odpowiedzialna będzie za kontrolę, co faktycznie ma zostać wyświetlone w widoku. Warstwa danych zostanie zatem odseparowana od samego prezentera.

Rysunek 5. Struktura projektu MVVM

W tym celu niezbędna jest nowy plik, który nazwałem ToDoViewModel. Będzie on odpowiedzialny za przechowywanie elementów w odpowiedniej kolejności (zdejmuje tym samym odpowiedzialność z dataManagera) i tym zajmie się klasa o tej samej nazwie. Dodatkowo muszę stworzyć nową klasę dla elementu Todo, która będzie jej reprezentantem na froncie oraz będzie odpowiedzialna za przygotowanie elementów w prawidłowym formacie, tzn. sprawdzeniu warunków takich jak status elementu, jego priorytet i zamiana daty na tekst.

Tworzę listę elementów gotowych do wyświetlenia, posortowanych według założeń biznesowych.

Tworzę metody do zarządzania elementami listy (dodawanie, odejmowanie, modyfikacja).

Tworzę obiekt przechowujący wszystkie niezbędne elementy, które będą prezentowane w widoku (tj. sformatowany tekst, kolor priorytetu, data w formacie tekstowym).

W samym kontrolerze zbyt dużo się nie zmienia (przynajmniej na tym etapie), ale zyskujemy odseparowanie od dataManagera, co w przyszłości pozwoli nam bez ingerencji w warstwę prezentacji zmieniać implementację warstwy zarządzania danymi.

Tworzę referencję do viewModel w kontrolerze.

Wykorzystanie viewModel’u na przykładzie wyświetlania komórki w tableView.

Funkcja konfigurująca komórkę w TodoItemTableViewCell.

Porównując zatem te dwie funkcję we wzorcu MVVM z wzorcem MVC zyskaliśmy większą przejrzystość kodu, odseparowanie widoku od modelu danych, a tym samym możliwość zarządzania formą prezentacji bez konieczności ingerencji w dane.

Podsumowanie

Przyszedł czas na krótkie podsumowanie. W programowaniu aplikacji mobilnych, klasy obsługujące dane widoki w modelu MVC w bardzo szybkim tempie zwiększają swój rozmiar co stwarza problemy w późniejszym utrzymaniu takiego kodu i jednocześnie zaciera obraz logiki biznesowej. O wiele lepszym rozwiązaniem dla aplikacji mobilnych (i nie tylko) staje się zastosowanie wzorców projektowych, które w większym stopniu rozbijają kod na mniejsze, logicznie spójne elementy, jednocześnie separując warstwy od siebie nową warstwą abstrakcji.

Oczywiście każdy wzorzec ma swoje plusy i minusy, więc do danego projektu należy podchodzić indywidualnie i przed rozpoczęciem prac rozważyć czy to rozwiązanie jest najlepszym z możliwych. Niemniej, wzorzec MVVM coraz bardziej zyskuje na popularności i warto zapoznać się z nim nieco bliżej, a na pewno w przyszłości odwdzięczy się nam skróceniem czasu przy dorabianiu nowego ficzera do naszej aplikacji. Dodatkowo zyskujemy o wiele większy procent pokrycia kodu testami w porównaniu do MVC.

Tymczasem już teraz zapraszam Was do przeczytania kolejnego artykułu z tej serii, który już niebawem powinien się ukazać na tym portalu. Poruszę w nim temat reaktywności aplikacji oraz na przykładzie pokaże w jak można wykorzystać bibliotekę RxSwift.

Poniżej możecie przeanalizować sobie cały projekt na moim githubie:

Link do projektu MVC

Lind do projektu MVVM


Zdjęcie główne artykułu pochodzi z unsplash.com.

Patronujemy

 
 
Polecamy
W jakiego typu projektach mobilnych wybrać Xamarin.Native?