dobre praktyki

Wzorzec MVVM Wyjaśniony w Jednym Artykule

Wzorce projektowe są sprawdzonymi szablonami, które z możemy wykorzystywać w swoich projektach. Wzorzec MVVM jest jednym z najbardziej popularnych wzorców, z którymi możesz spotkać się w swojej karierze programisty.

W tym artykule zawarłem odnośniki do aplikacji mobilnych, ale sam wzorzec jest niezależny od technologii.

Jedziemy z tematem.

Koncepcja wzorca MVVM

Jego pełna nazwa brzmi ModelViewViewModel. Całość opiera się na wydzieleniu trzech logicznych warstw w systemie. Chodzi o solidny podział odpowiedzialności, czyli pomiędzy klasami. Co w konsekwencji prowadzi do tego, że każda warstwa ma swoje ograniczenia. I te właśnie ograniczenia pozwalają nam na pisanie lepszego kodu.

Diagram opisujący działanie wzorca MVVM. 

Model

Zacznijmy warstwy modelu. Jest to najniżej położona warstwa, w której to znajdziesz dane i reguły biznesowe aplikacji. Znajdują się tutaj te klasy, których zadaniem praca np. z bazą danych, pewne skomplikowane operacje np. komunikacja z API. Co oznacza, że znajduje się tutaj domena aplikacji i infrastruktura.

☝️
Mówiąc o najniżej warstwie, mam na myśli, że znajduje się ona najbliżej surowych danych. 

Domena aplikacji

Czym jest domena? W kilku żołnierskich słowach odzwierciedla to, czym jest nasza aplikacja. Załóżmy, że uznałeś, że chcesz przebić eBay’a i tworzysz własny sklep. W takim przypadku przykładowymi encjami mogą być: Order, Client, Product.

To, co jest tutaj ważne, to to, że podczas ich tworzenia musisz pamiętać, aby były one czyste. To znaczy, że powinieneś korzystać tylko z typów wbudowanych lub innych, istniejących już typów domenowych. Byłoby świetne, aby te typy nie korzystały wprost z dostępu do danych. One powinny w pewnym sensie odzwierciedlać reguły biznesowe, ale nie powinny w sobie posiadać logiki przetwarzania niskopoziomowych rzeczy.

Rzeczy takie jak: logika pobierania danych użytkownika, zapis do bazy nie są odpowiednim miejscem dla encji. Takie rzeczy powinny znaleźć się w repozytoriach, czy serwisach – czyli w infrastrukturze. Encje domenowe powinny (o ile jest to rzeczywiście nieuniknione) korzystać w takim razie z abstrakcji. Byłoby jeszcze lepiej, gdyby odwrócić zależność i to serwisy domenowe operowały na modelach domenowych, ale to temat na inny wpis.

🙆🏻‍♀️
Tworzenie odpowiedniego nazewnictwa, nadawanie wartości konkretnemu modelowi lub ich grupie zajmuje się DDD, czyli Domain-Driven design.
class User
{
    public User(string fullName, int age)
    {
        FullName = fullName;
        Age = age;
    }

    public string FullName { get; }

    public int Age { get; }
}

Dostęp do danych

Warstwa dostępu do danych zajmuje się, (uwaga!) dostępem do danych. Znajdują się tutaj m.in. repozytoria (czyli klasy odpowiedzialne za zwracanie danych za pomocą modeli domenowych z jednego/kilku źródeł) lub serwisy infrastrukturalne lub domenowe, które operują na danych i je przetwarzają. Dla uproszczenia można powiedzieć, że TO na czym operuje model widoku (warstwa wyżej), można zaliczyć do tej warstwy.

🙆🏻‍♀️
Gwoli ścisłości.

Istnieją różne sposoby na podział odpowiedzialności, dlatego powyższe zdanie nie do końca jest prawdziwe. Czasami chcemy wyciągnąć część logiki z modelu widoku do innej odpowiedzialności.

W takim przypadku ta zależność nie należy więc do warstwy dostępu do danych.

Widok

Wzorzec MVVM posiada bardzo dobrze widoczną warstwę, czyli warstwę widoku. Oczywiście jest ona odpowiedzialna za wizualną reprezentację oraz interakcję z użytkownikiem. Wydaje się najłatwiejsza.

📱
Android
W przypadku Androida znajdzie się tutaj fragment/activity wraz z definicją pliku w XML lub własna stworzona kontrolka. W bardziej nowoczesnym podejściu warstwą widoku nazwiemy pliki korzystające z Android Compose.

Swoją drogą, jeśli interesujesz się Androidem, to polecam Ci mailing Mateusza, który znajdziesz pod poniższym adresem.
📱
iOS
Jeśli mowa o iOS, będzie to kod zamieszczony w klasie dziedziczącej po ViewController, interfejs stworzony w xCode Interface Builder. Ewentualnie tak jak w przypadku Androida, w bardziej nowoczesnym wydaniu, tam, gdzie znajdzie się kod korzystający ze SwiftUI.

O czym należy pamiętać?

Widok ma referencję do modelu widoku, nie odwrotnie. Pod żadnym pozorem nie możesz przekazywać instancji widoku (lub jakiejkolwiek jego składowej) do modelu widoku.

To cholernie ważne. W MVVM każda warstwa ma narzucone zasady i ograniczenia, których powinniśmy przestrzegać, aby zmniejszyć zależności (ang. coupling) pomiędzy nimi.

Komendy

Komunikacja z warstwą modelu widoku odbywa się za pomocą komend oraz data-bindingu. Zadaniem komend jest uruchomienie jakiejś akcji. Widok może taką komendę tylko wywołać (metoda Execute()) lub w rozszerzonej wersji sprawdzić, czy akcja jest możliwa do wywołania (metoda CanExecute()). Przykład? Najprostszy przycisk. Zawsze wywołuje jakąś akcję i może być w jakimś stanie: zablokowany lub dostępny.

Komenda jest zwykłą klasą, która posiada dwie, wcześniej przedstawione metody oraz możliwość przekazania akcji (delegata), która ma zostać wywołany, gdy komenda zostanie o to poproszona.

Z perspektywy widoku, nie wiadomo, jaka metoda zostanie wywołana (delegaty, pisałem o nich w innym poście). Widok może tylko nakazać jej wykonanie komendzie. Służy to hermetyzacji kodu, czyli ukrywania implementacji poza jakimś kontekstem.

Przykład? Najprostszy przycisk. Zawsze wywołuje jakąś akcję i może być w jakimś stanie: zablokowany lub dostępny.

public ICommand LoginCommand => new Command(NavigateToMainScreen); 
// NavigateToMainScreen jest delegatem, który zostanie wywołany.

W sumie nie stanie się nic złego, jeśli z poziomu widoku wywołasz po prostu metodę modelu widoku. Komenda nie musi być jedynym rozwiązaniem.

Szkoła C# - Delegaty i wyrażenia lambda
W tym wpisie chciałbym przedstawić Ci, czym są delegaty oraz wyjaśnić korzystanie z wyrażeń lambda w języku C#
Znajdź książkę o wzorcu MVVM w internetowej księgarni Helion
Księgarnia internetowa informatyczna Helion.pl - wydawnictwo informatyczne, książki, kursy
Księgarnia informatyczna helion.pl - informatyka w najlepszym wydaniu. Lider na rynku literatury informatycznej. W księgarni można zapoznać się z ofertą Wydawnictwa Helion (opisy, fragmenty książek, opinie czytelników, spisy treści, recenzje).

Data binding

Pozwala na stałą komunikację pomiędzy modelem widoku a widokiem. Dzięki niemu (a raczej frameworkowi, który go udostępnia) nie musimy ręcznie odświeżać danych. Wystarczy, że zgodnie z wymaganiami konkretnego frameworka odpowiednio zwiążemy jedną z właściwości wybranego widoku z konkretną właściwością w modelu widoku. Po naszej stronie jest tylko dać znać w modelu widoku, że dane uległy zmianie. Binding zrobi resztę.

var set = this.CreateBindingSet<LoginView, LoginPO>();

set.Bind(_emailTextView)
    .For(v => v.Text)
    .To(po => po.Email);

set.Bind(_passwordTextView)
    .For(v => v.Text)
    .To(po => po.Password);

set.Apply();

Model widoku

Ostatnim składnikiem jest warstwa modelu widoku. Jej zadaniem jej udostępnienie danych dla widoku oraz wymianę informacji z modelem. To oznacza pobieranie danych oraz jego aktualizację. Sam model widoku udostępnia i zawiera tylko to, czego potrzebuje widok.

Jest to skrojony pod niego dostawca treści i zachowań.

Załóżmy, że tworzysz ekran logowania. Wtedy znajdą się tutaj właściwości przechowujące aktualnie wpisany email oraz hasło użytkownika. Dodatkowo dodasz tutaj również wcześniej wspomnianą komendę dla akcji logowania.

41 pytań, które musisz zadać podczas rekrutacji.
Lista pytań, które musisz zadać podczas rekrutacji jako programista. Rekruter będzie sprawdzał, czy umiesz programować. Ty zadaj te pytania.

Musimy mieć pewność, że widok dysponuje aktualnymi danymi. Aby sobie z tym poradzić, należy skorzystać z interfejsu INotifyPropertyChanged (lub jego jakiś odpowiednik w zależności od frameworka), jego zadaniem zgodnie z nazwą jest powiadamianie o zmianach. To ten interfejs jest wykorzystywany w widoku to jego odświeżenia (patrz data binding). Ty jako programista musisz tylko zadbać o to, aby powiadomić o zmianach.

Wzorzec MVVM. Co o nim sądzisz?

W ilu projektach miałeś okazję pracować z tym wzorcem? W moim przypadku wzorzec MVVM głównie wykorzystywałem w projektach Xamarin.Native i moim zdaniem jest to wzorzec bazowy, jeśli chodzi o pracę z tą technologią.

Masz jakieś przemyślenia? Podziel się z nimi w komentarzu.

Tagi