mvvm od środka « Pabloware + Windows Phone

Pabloware + Windows Phone

Niezależny blog firmy Pabloware o systemie Windows® Phone 7

Wzorzec architektoniczny Model View ViewModel (2/2)

Warstwa View

Ostatnia do omówienia we wzorcu architektonicznym MVVM jest warstwa View. Jest to warstwa odpowiedzialna za wygląd interfejsu użytkownika aplikacji. Jako pojedynczy element warstwy View (czyli jeden widok), w przypadku aplikacji dla Windows Phone 7 rozumie się stronę aplikacji, choć jest to granica umowna, bo można też pod tą nazwą rozumieć rozbudowaną kontrolkę niestandardową, okno wyskakujące, itp.

W technologiach Silverlight i WPF warstwa View składa się z dwóch części. Podstawową częścią jest plik XAML, zawierający deklaratywny opis interfejsu użytkownika. Opcjonalną częścią jest plik code-behind, w którym może zostać umieszczony kod programu, związany z tym interfejsem użytkownika.

Część warstwy View, zawarta w pliku XAML, jest zawsze niezależna od warstwy ViewModel. Wskazuje się w niej wprawdzie konkretne miejsca warstwy ViewModel, ale nie jest ona kompilowana, nie może więc być z naszego punktu widzenia zależna. Co więcej, można przygotować dla niej testowe dane statyczne, używane podczas graficznego projektowania widoku.

Sprawia to, że możliwe jest całkowite oddzielnie roli projektanta interfejsu użytkownika od roli programisty, bo projektant nie musi mieć dostępu do stworzonego przez programistę kodu źródłowego programu, żeby stworzyć widok interfejsu użytkownika, od razu wiążąc go z przygotowaną wcześniej przez programistę warstwą ViewModel. Jest to często podawana zaleta wzorca architektonicznego MVVM i formatu XAML, choć oczywiście jest prawdziwie przydatna jedynie w odpowiednio dużych i złożonych pod względem graficznym projektach. W małych lub prostych graficznie projektach, w których programista często nie korzysta nawet z graficznego trybu projektowania, ręcznie pisząc kod XAML, jest ona, zdaniem autora, pomijalna.

Aby uzyskać wiązania danych z kontrolką interfejsu użytkownika, deklaruje się je w pliku XAML, przy czym dane te pochodzą z warstwy ViewModel, nazywanej źródłem wiązania i są wiązane z tzw. celem wiązania. Pod względem implementacji, warstwa ViewModel, jedynie musi udostępnić te dane w taki sposób, aby warstwa View była informowana o ich zmianie, co czyni się implementując bardzo prosty interfejs INotifyPropertyChanged. Z kolei kontrolki interfejsu użytkownika muszą dziedziczyć po klasie DependencyObject i używać klasy DependencyProperty, aby ich właściwości mogły być wiązane.

Oprócz wiązania danych, w pliku XAML możliwe jest również „wiązanie” zachowań aplikacji (np. reakcji na naciśnięcie przycisku), poprzez wykorzystanie mechanizmu komend (ang. command). Jednak jedyną korzyścią z takiej strategii jest możliwość „wiązania”, przez projektanta interfejsu użytkownika (a nie tylko przez programistę), zachowań aplikacji z poszczególnymi elementami interakcji z interfejsem użytkownika. Poważną wadą mechanizmu komend jest ilość kodu, jaka musi zostać, na etapie programowania, dodana do projektu, aby udostępnić pojedynczą komendę, co powoduje niepotrzebne komplikowanie projektu. Użycie komend powoduje także niepotrzebne spowolnienie aplikacji pod systemem Windows Phone 7.

W związku z powyższym, autor rezygnuje na ten moment z użycia komend, gdy nie niosą one żadnych korzyści, a mają poważne wady. Jedynym sposobem wiązania interfejsu użytkownika z zachowaniami udostępnianymi przez warstwę ViewModel, pozostaje wtedy plik code-behind.

Plik ten, jak już wcześniej napisano, jest opcjonalny i w „ekstremalnej” wersji wzorca architektonicznego MVVM jest prawie lub całkowicie pusty. Jednak umieszczenie w tym pliku bezargumentowych, pojedynczych wywołań, odpowiednio przygotowanych metod klas warstwy ViewModel, w metodach obsługi zdarzeń interfejsu użytkownika, nie wprowadza żadnej dodatkowej komplikacji. Jest dokładnie tym samym, pod względem np. możliwości testowania kodu, co „wiązanie” komend, ale pozbawionym wymienionych wad komend.

Innym przykładem kodu, który według wielu osób można umieszczać w pliku code-behind, jest kod operujący tylko i wyłącznie na interfejsie użytkownika, bez bezpośredniego związku z logiką aplikacji. Do tej kategorii należą np.: pokazywanie okien dialogowych, uruchamianie dekoracyjnych animacji lub ustawianie ogniska na kontrolce.

Można wyobrazić sobie nawet rezygnację z mechanizmu automatycznych wiązań danych i w konsekwencji ręczne przepisywanie, w pliku code-behind, danych z obiektu ViewModel do kontrolek interfejsu użytkownika. W środowisku Windows Phone 7 niosłoby to za sobą pewną korzyść wydajnościową, ale powodowałoby rozbudowanie i utrudnienie w zarządzaniu plikiem code-behind. Wprawdzie pod względem testowania kodu nie różniłoby się niczym od stosowania wiązań danych w pliku XAML, ale tylko przy założeniu, że dane te byłyby statyczne. Obsługiwanie zmiany danych wprowadza bowiem trudną do zaakceptowania, w nietestowanej warstwie View, komplikację kodu.

Podsumowanie

Wzorzec architektoniczny MVVM niesie za sobą możliwość podziału aplikacji na luźno ze sobą połączone warstwy. Powoduje to, że aplikacje stają się bardziej rozszerzalne i łatwiejsze w zarządzaniu. Umożliwia także pełniejsze ich testowanie. Sprzyja ich modularyzacji, mającej na celu lepszy podział pracy nad aplikacją.

Elastyczność tego wzorca projektowego zachęca do stosowania go w wyważony sposób, w zgodzie z możliwościami środowiska, takiego jak np. telefon komórkowy. Chociaż dostępnych jest wiele, bardzo się od siebie różniących, gotowych bibliotek ułatwiających implementację tego wzorca, dostarczających do tego celu gotowego szkieletu, autor na ten moment nie decyduje się na skorzystanie z żadnej z tych bibliotek, ponieważ w ten sposób można wybrać z tego wzorca najlepsze elementy, nie obciążając budowanych aplikacji dodatkową biblioteką.

Za zastosowaniem tego wzorca architektonicznego w aplikacjach, przemawia głównie możliwość wykonywania testów jednostkowych na praktycznie całym kodzie aplikacji. Albowiem można w ten sposób podnieść niezawodność aplikacji. Również istotna jest duża rozszerzalność stosującego ten wzorzec kodu, gdyż umożliwia to jego łatwe rozbudowywanie. Ponadto trzeba podkreślić, że wzorzec ten może sprzyjać stosowaniu dobrych zasad projektowych, choć niesie w tym zakresie też zagrożenia.

Ponadto można spotkać się z opiniami, że zrozumienie podstawowych zasad wzorca MVVM, nawet gdy nie zamierzamy stosować go w praktyce, pozwala lepiej zrozumieć filozofię bibliotek WPF i Silverlight. Zrozumienie biblioteki Silverlight jest niezbędne do tworzenia aplikacji użytkowych, przeznaczonych dla systemu Windows Phone 7.

Wzorzec architektoniczny Model View ViewModel (1/2)

Zastosowanie na platformie Windows Phone 7 technologii Silverlight, pociąga za sobą popularność na niej wzorca architektonicznego MVVM. Mianowicie czytelnik dowie się, że wzorzec architektoniczny MVVM jest zdecydowanie wygodniejszy w stosowaniu, kiedy możliwe jest deklaratywne wiązanie danych (ang. binding) z kontrolkami interfejsu użytkownika (co nie znaczy, że nie można go stosować bez tego mechanizmu). Mechanizm takiego wiązania jest częścią bibliotek Silverlight oraz WPF, więc nic dziwnego, że z myślą o nich, ten wzorzec architektoniczny został opracowany i jest razem z nimi stosowany.

Można zaryzykować twierdzenie, że drugą przyczyną popularności wzorca architektonicznego MVVM na platformie Windows Phone 7 jest podział interfejsu użytkownika aplikacji na tej platformie na odrębne jednostki zwane stronami, co ułatwia zastosowanie tego wzorca, upraszczając identyfikację jego elementów.

Kod aplikacji o architekturze zgodnej ze wzorcem MVVM podzielony jest zgodnie z nazwą wzorca na trzy warstwy, które zostaną omówione poniżej oraz w następnym wpisie, uszeregowane według ich postępującej zależności.

Nakładanie się warstw wzorca architektonicznego MVVM

Warstwa Model

Zgodnie z nazwą, znajdują się tutaj klasy reprezentujące model biznesowy aplikacji, podobnie jak ma to miejsce, w tak samo nazwanej warstwie wzorca architektonicznego MVC (Model View Controller). Stosując zasadę pojedynczej odpowiedzialności (wzorzec projektowy SRP) i elementarne zasady projektowania obiektowego, dochodzimy do potwierdzenia, że w warstwie Model powinien znaleźć się wyłącznie kod modelu danych, na których działa aplikacja oraz kod zachowań tego modelu (logiki biznesowej).

Najbardziej oczywistą częścią klas należących do warstwy Model omawianego wzorca będą właściwości, które odpowiadają konkretnym danym, na których operuje aplikacja. Pojawia się tutaj ryzyko traktowania tych klas, jako prostych rekordów, służących do przenoszenia danych. Jak często w przypadku tego wzorca architektonicznego bywa, jest to kwestia nie do końca ustalona, ale można zasugerować czytelnikowi, traktowanie klas modelu, jako pełnowartościowych klas, a więc udostępniających, tam gdzie to możliwe, dane w postaci znaczącej dla reszty aplikacji, a nie jako dane surowe. Zgodnie, bowiem z dobrymi zasadami projektowania, gdybyśmy potraktowali klasy modelu, jako rekordy danych, nie moglibyśmy uzupełniać ich zachowaniami logiki biznesowej, bo doprowadziłoby to do mieszania obiektów z rekordami. Jeśli to konieczne, można w celu zachowania obiektowości modelu, umieścić dane w rekordach, które będą dołączone do obiektów modelu. Trzeba mieć jednak świadomość, że wprowadzi się tym kolejną warstwę do architektury aplikacji, nieprzewidzianą w powszechnie rozumianym wzorcu architektonicznym MVVM. Wydaje się, że jest to obszar wzorca do pilnego doprecyzowania, jeśli chcielibyśmy aby wzorzec był ścisły.

Innym, często spotykanym przykładem kodu, który umieszcza się w klasach warstwy Model, jest walidacja danych, bowiem można ją zaliczyć do logiki biznesowej. Autor umieszcza w tej warstwie również np. kod dokonujący obliczeń, co nie budzi wątpliwości, pod względem zgodności ze wzorcem, jeśli są to obliczenia należące do logiki biznesowej aplikacji. W klasach należących do warstwy Model można umieszczać również odnajdywanie referencji do powiązanych elementów modelu, zapewniając tym samym spójność odnajdywania modelu na poziomie samego modelu, przy czym właściwa praca odnajdywania może znaleźć się w klasie pomocniczej.

Podsumowując, można pokusić się o założenie, że w warstwie Model powinien znaleźć się taki kod logiki biznesowej aplikacji, który będzie miał jak najbardziej bezpośrednie zastosowanie w innych warstwach aplikacji, jako sedno jej funkcjonalności, ale nie, jako kod pomocniczy, np. związany z konkretnym środowiskiem uruchomieniowym aplikacji. W związku z tym kod modelu może być użyty bez żadnych zmian np. w wersjach biurkowej i webowej tej samej aplikacji. Wynika też z tego, że warstwa Model jest całkowicie niezależna od dwóch pozostałych warstw omawianego wzorca architektonicznego.

Warstwa ViewModel

Odpowiedzialnością klas znajdujących się w warstwie ViewModel jest dostarczenie z i do interfejsu użytkownika danych i zachowań, udostępnianych przez warstwę Model, w postaci, która przed zastosowaniem w interfejsie użytkownika, nie wymaga już żadnej modyfikacji. Oczywista jest związana z tym zależność pośredniczącej ze swej natury warstwy ViewModel od warstwy Model. Jednak zależność warstwy ViewModel od interfejsu użytkownika nie jest już tak oczywista. Spróbujmy poznać tę zależność.

Z pewnością warstwa ViewModel nie powinna bezpośrednio korzystać z interfejsu użytkownika, aby możliwa była jej kompilacja i testowanie bez posiadania tegoż (sposób odseparowania warstwy ViewModel od interfejsu użytkownika zostanie opisany przy okazji omawiania warstwy View). Jest bowiem, jedną z największych zalet stosowania wzorca architektonicznego MVVM, możliwość stosowania testów jednostkowych, na praktycznie całej programowej części aplikacji (warstwach Model i ViewModel). Gdyby warstwa ViewModel była bezpośrednio zależna od interfejsu użytkownika, stosowanie wobec niej testów jednostkowych byłoby z samej ich natury mocno utrudnione. Na takie trudności niestety natrafiamy nie stosując wzorca MVVM, bądź też innych metod izolacji interfejsu użytkownika od reszty aplikacji.

Z drugiej strony warstwa ViewModel jest funkcjonalnie całkowicie podporządkowana interfejsowi użytkownika. W związku z tym, przyjmuje się ogólną zasadę, że jednemu widokowi w interfejsie użytkownika (takiemu, jak np. okno z listą danych), powinna odpowiadać jedna klasa należąca do warstwy ViewModel.

Jednak najbardziej podstawowym przypadkiem klasy należącej do warstwy ViewModel, jest klasa opakowująca poszczególne właściwości odpowiadającej jej klasy, należącej do warstwy Model. Często właściwości te nie są przekazywane do interfejsu użytkownika w identycznej, jak obecna w warstwie Model postaci, co związane jest z koniecznością np. konwersji ich na inny typ danych lub też innej formy doprowadzenia ich do postaci, nadającej się do wyświetlenia w interfejsie użytkownika, np. poprzez zmianę ustrukturyzowania danych.

Dla porządku warto przypomnieć, że chodzi tu jedynie o przygotowanie danych do wyświetlenia, a nie np. o obliczenia na danych. Bowiem te, według przyjętego w tym wpisie rozumienia wzorca architektonicznego MVVM, winny odbyć się jeszcze w warstwie Model.

Innym mechanizmem, służącym do konwersji danych w celu ich wyświetlenia, są konwertery, wspomniane w poprzednim wpisie. Z powodów tam przedstawionych nie są one chętnie wykorzystywane przez autora, a ich zadanie jest oczywiście zlecane warstwie ViewModel. Co więcej wydaje się, że stosowanie konwerterów razem z warstwą ViewModel jest całkowicie zbędne, skoro duplikują one odpowiedzialność warstwy ViewModel. Ich użyciu może sprzyjać przesunięcie odpowiedzialności z warstwy Model na warstwę ViewModel lub bezpośrednie udostępnienie klas należących do warstwy Model w warstwie ViewModel, lecz oba nie są chyba zjawiskami właściwymi, z punktu widzenia zgodności ze wzorcem.

Z powyższego opisu dobrych klas należących do warstwy ViewModel, łatwo wyodrębnić dwa typy tych klas:

  • klasy odpowiadające podstawowej jednostce interfejsu użytkownika, np. stronie (ViewViewModel);
  • klasy odpowiadające typom warstwy Model (ModelViewModel).

Trzeba zaznaczyć, że nie jest to podział powszechnie przyjęty we wzorcu MVVM (brak jest nawet nazw tych klas, stąd w nawiasach proponuje się nazwy robocze), choć jego użycie przez autora nie jest odosobnione.

Jak łatwo się domyślić, klasy typu ViewViewModel udostępniają obiekty klas typu ModelViewModel, przy czym w zależności od rodzaju obsługiwanego interfejsu użytkownika, mogą udostępniać pojedynczy obiekt klasy typu ModelViewModel lub całą ich listę. Bowiem pełnią one często rolę pomocniczą do klas typu ModelViewModel, udostępniając operacje takie jak dodawanie i usuwanie elementów, aby klasy typu ModelViewModel mogły ograniczać się do „tłumaczenia” klas modelu na interfejs użytkownika.

Jedyną wadą stosowania tego podziału jest konieczność posiadania dwóch klas warstwy ViewModel, gdzie czasem wystarczyłaby jedna, co prowadzi niekiedy do powstania dużej liczby klas. Wydaje się jednak, że jest to niska cena, wobec uzyskania zgodności z zasadą pojedynczej odpowiedzialności.

Jak zapewne czytelnik pamięta, warstwa Model jest całkowicie przenośna. Ciekawym wobec tego, nieomówionym jeszcze tematem, jest przenośność warstwy ViewModel. Spróbujmy odpowiedzieć sobie na pytanie: czy warstwa ViewModel może być przenoszona między platformami?

Można wyobrazić sobie, że na każdą platformę, na którą dostarczana będzie aplikacja, przeznaczona będzie osobna implementacja warstwy ViewModel. Taka zasada jest logiczna, gdy różnice w interfejsie użytkownika pomiędzy platformami są znaczące (np. strona internetowa w porównaniu do aplikacji na telefon komórkowy). Z drugiej strony, gdy różnice są relatywnie niewielkie (np. aplikacja na system Windows w porównaniu do aplikacji na system Mac OS), można wyobrazić sobie zastosowanie wspólnej implementacji warstwy ViewModel na obu platformach, co znakomicie przyczyniłoby się do zwiększenia łatwości budowy aplikacji, przeznaczonych na wiele platform.