Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc

M-V-P Twoim przyjacielem

Będąc na studiach zacząłem pracę programisty. Głowę miałem wypakowaną teorią, a w duszy grała chęć zastosowania tego wszystkiego w praktyce. Każdy kto czytał o wzorcach projektowych GoF wie jak bardzo zmienia ona postrzeganie i jak bardzo zachęca nas do wykorzystywania tychże wzorców w praktyce. Niestety od nadmiaru wzorców głowa boli. Pewne mało skomplikowane aplikacje zupełnie ich nie potrzebują, a wprowadzenie ich tylko niepotrzebnie zaciemnia obraz sytuacji. Zatem wybór wzorca i to, czy rzeczywiście jego zastosowanie ma sens, poprzeć trzeba pewnym uzasadnieniem.

Zaczynając pracę trafiłem do zespołu, który buduje aplikację w oparciu o Windows Forms, znaczy się to co tygryski lubią najbardziej. Już na samą myśl o WF w głowie zapala mi się lampka - MVP. Wcześniej nie miałem okazji wypróbować go w boju. W małych projektach studenckich stosowałem, ale z rzeczywistych jego możliwości nigdy nie korzystałem. Co więcej, nie do końca zdawałem sobie sprawy z jego potęgi!

W tekście tym chciałem Wam opowiedzieć o tym jak na co dzień wykorzystuję ten wzorzec i jakie wymierne korzyści dzięki niemu uzyskałem.

Grunt to dobry podział

Dla wielu implementacja wzorca MVP sprowadza się do tego, że będą mogli podmieniać widoki. Casami ludzie mówiąc o zaletach MVP wskazują na to, że można tak napisać aplikację wykorzystując ten wzorzec, że będzie można wymiennie stosować Windows Forms oraz ASP.Net. Jakoś nie widziałem, żeby się to komuś udało. Nie twierdzę że nie jest to możliwe, ale czy rzeczywiście o to chodzi?

Spójrzmy może na ten wzorzec z innej strony. Wprowadza on podział na trzy elementy: Model, Widok i Prezentera. Zapytać ktoś może po co taki podział? Czemu nie mogę oprogramować okna w jego klasie? Nie mając perspektyw dalszego rozwoju aplikacji moglibyśmy tak zrobić, jednak praktyka sugeruje, że aplikacja będzie rosła a klient będzie wymyślał nowe problemy do rozwiązania.

Przede wszystkim oddziel zagadnienia (ang. Separation of Concerns, SoC). Wprowadzenie tych trzech elementów nie jest przypadkowe. Widok to reprezentacja wizualna danych – która może mieć wiele postaci np. okno czy strona www. Prezenter to logika reprezentacji danych. Definiuje zachowanie widoku np. reaguje na akcje użytkownika. Na końcu model to dane. Czasami danymi jest pojedynczy obiekt, innym razem może to być zbiór obiektów. Promując zasadę SoC rozdzielamy te zagadnienia umieszczając je w osobnych klasach.

Podmiany

Dobrze, mamy już jakieś wymierne korzyści, które w dowolnej wielkości aplikacji będą procentować. Zastanówmy się teraz jak z tego wzorca wyciągnąć jeszcze więcej. W pracy niejednokrotnie spotkałem się z problemem ponownego wykorzystania komponentów. Weźmy mały przykład, który z powodzeniem odnajdziecie w wielu rozwiązaniach klasy ERP, CRM lub podobnych. Chodzi mi o listę kontrahentów. Z pozoru problem jest prosty – należy umożliwić zarządzanie kontrahentami w aplikacji. Sprowadza się to do zaimplementowania listy kontrahentów, a także szczegółów konkretnego kontrahenta. Skupmy się jednak na samej liście. Zadanie banalne – mamy tabelę w bazie danych zawierającą dane o kontrahentach. Nasza lista będzie pobierała wszystkie dane z tej tabeli i wyświetlała w tabeli w oknie programu.

Mija czas i przychodzi następne wymaganie. Należy umożliwić wybór kontrahenta z listy. Sytuacja dotyczy wszystkich dokumentów handlowych aplikacji, w których trzeba wybrać kontrahenta. Oczywiście system musi być spójny więc lista musi wyglądać i zachowywać się identycznie (możliwość dodawania, edycji itd.). Zabieramy się za implementację. Tę samą formatkę wzbogacamy o właściwość IsInSelectionMode, którą ustawiać będziemy na true, gdy będziemy wyświetlać listę w trybie wyboru. Wewnątrz dodajemy odpowiednie if-y, aby zmienić działanie formatki np. jednym z wymagań jest to, aby dwuklik na wierszu powodował wybór w trybie wyboru, a edycję w trybie zwykłym. Wszystko śmiga.

Znów mija trochę czasu. Przychodzi klient i mówi: jak wpiszę tylko część nazwy klienta a w bazie jest więcej niż jeden klient o nazwie zaczynającej się na wpisaną frazę to chcę aby pojawiła się lista klientów zawężona tylko do tych pasujących do frazy. Znów przystępujemy do implementacji. Gdy użytkownik wpisze żądaną frazę strzelamy do bazy i mamy już listę pasujących kontrahentów. Zgodnie z warunkiem, jeżeli jest ich więcej niż jeden otwieramy listę. Sama lista też wymaga pewnych modyfikacji. Mając zestaw danych w ręce (listę pasujących kontrahentów) nie chcemy aby lista sama pobierała dane, tylko chcemy jej podać to co już mamy. Dodajemy kolejne pole i przypisujemy mu zawartość listy kontrahentów zaczynających się od frazy. W samej liście znów dodajemy if-y, tak aby spełnić wymagania. Teraz jeżeli nowo dodane pole nie jest puste nie pobieramy wszystkich kontrahentów z bazy, a jedynie wyświetlamy tych otrzymanych.

Spoglądając wstecz zauważymy, że stworzyliśmy coś, co na dłuższą metę może być uciążliwe w utrzymaniu. Nie dość, że wszystkie funkcje dotyczące obsługi listy są w jednej klasie to jeszcze sama logika się skomplikowała, ponieważ wprowadzono warunki if. Każda z komend dotycząca zawartości wyświetlanej listy np. ‘odśwież’ musiała zostać odpowiednio zmodyfikowana, aby zapewnić poprawność, bo przecież nie możemy pobierać danych z bazy danych mając zadaną stałą listę. To prowadzi do większego prawdopodobieństwa popełnienie błędu, albo wprowadzenia błędu przy późniejszych modyfikacjach.

Co więcej z punktu widzenia programisty, który naszej listy będzie używał, jej API nie do końca jest intuicyjne. Musi wiedzieć, że właściwość IsInSelectMode przełącza listę w tryb wyboru, a ustawienie pola ExternalDataSource powoduje zmianę zachowania listy i wyświetlenie tylko zadanego zestawu danych.

Rozwiązanie takie tworzy wiele problemów. Nie ma jasno powiedziane gdzie znajduje się źródło danych. Informacja ta jest rozproszona po kodzie całej kontrolki. Nie ma też jasno określonego dostawcy danych. Lista w standardowym trybie sama zaczytuje sobie dane, ale w przypadku trybu zawężonej listy, dane pobierane są z zewnątrz.

Z punktu widzenia utrzymania takiego komponentu mamy kolejny problem. Promując zasadę otwarty-zamknięty (ang. Open-Closed Principle, OCP) chcielibyśmy, aby dodawanie nowych funkcji mogło się odbyć bez modyfikacji już istniejących klas. W tym wypadku dodanie kolejnego trybu (a nie zdziwiłbym się, gdyby poproszono o coś jeszcze) wiąże się z nieustannym modyfikowaniem tej samej klasy, która szybko przyjmie rozmiary kilku tysięcy linii kodu.

W tym przypadku i wielu innych, które spotkałem podczas pracy z pomocą przyszedł mi wzorzec MVP i podział wprowadzany przez jego komponenty. Tak jak pisałem wcześniej Prezenter określa nam klasę odpowiedzialną za zachowanie, a model dostarcza danych. Z powodzeniem możemy wykorzystać te klasy jako elementy wymienne.

Zobaczmy jak takie rozwiązanie mogłoby wyglądać. W tekście wytłuściłem elementy opisujące kolejne wymagania. Pierwsza zmiana polegała na zmianie zachowania formatki. Skoro zmienialiśmy zachowanie to w modelu MVP wystarczyłoby wymienić prezentera na takiego, który definiuje odrębne zachowanie. Wystarczyłoby mieć bazowego prezentera, który obsługuje wszystkie wspólne komendy takie jak dodawanie i usuwanie, a dwa dziedziczące po nim definiowały by obsługę dwukliku. Prawda, że proste?

Drugie wymaganie polegała na zmianie danych wyświetlanych na liście. Dlaczego nie wyodrębnić dwóch modeli. Pierwszy obsługiwałby wersję standardową wczytując wszystkich kontrahentów z bazy danych, a drugi przyjmowałby w konstruktorze gotowy zestaw danych. Prawda, że proste?

Teraz uruchomienie listy w żądanym trybie sprowadza się do wyboru odpowiedniego prezentera oraz odpowiedniego modelu. Na przykład chcąc uruchomić listę w trybie wyboru z zawężoną listą kontrahentów napisalibyśmy:

1 var view = new CustomerListView(); 2 var model = new ManualCustomerListModel(matchingCustomers); 3 var presenter = new CustomerSelectionPresenter(view, model);

Oczywiście nie moglibyśmy tego osiągnąć gdyby nie to, że każdy z tych komponentów opisany został dobrze określonym interfejsem. Dzięki temu całą trójkę możemy dowolnie komponować.

Wspomniałem wcześniej, że pierwsze rozwiązanie w ogóle nie spełnia zasady otwarty-zamknięty. Zobaczmy jaką sytuację mamy teraz. Każda z wariacji na temat listy kontrahentów zamknięta została w swojej własnej klasie. Zaimplementowanie kolejnego przypadku sprowadza się teraz do rozważenia jakie własności są modyfikowane. Jeżeli modyfikujemy zachowania to należy odpowiednio zaimplementować prezentera. Natomiast jeżeli modyfikujemy wyświetlane dane wprowadzamy nową implementację modelu. Co ważne to to, że nawet przez chwilę nie dotykamy klas już istniejących.

Podsumowanie

Przedstawiony tutaj problem dotyczący listy kontrahentów jest oczywiście dość prostym przypadkiem. W rzeczywistości spotkałem się z sytuacjami, gdzie zachowanie okna znacznie się różniło od jego podstawowej implementacji. Tak samo było z danymi. Nie implementowałem natomiast nigdy dwóch widoków mających prezentować wspólne dane, ale jak łatwo się domyślić nie powinno to nastręczać problemów przy zachowaniu jednolitego interfejsu.

Ci, którym nie jest obojętna wysoka jakość systemu zauważą, że implementacja testów jednostkowych dla tak zdefiniowanego systemu, nie powinna być skomplikowana. Projektując w ten sposób sprawiamy, że system jest testowalny.

Z własnych praktycznych doświadczeń mogę szczerze powiedzieć, że zastosowanie wzorca MVP oszczędziło mi wiele pracy, oszczędza i będzie oszczędzało w przyszłości, gdy przyjdzie mi rozszerzać bądź poprawiać błędy. Mając odpowiedni podział możemy dowolnie manipulować jego podzespołami, wymieniając je wedle uznania i komponując co raz to nowe rozwiązania. Tak zaprojektowany system jest niesamowicie elastyczny, zgodny z zasadą OCP, dzięki czemu daje się łatwo utrzymywać.

Dobrze określony podział obowiązków pozwala nam szybciej i bardziej mechanicznie podejmować decyzje o tym gdzie dana funkcja powinna się znaleźć. Znając naturę dużych i skomplikowanych systemów wiem, że implementacja jednej formatki może nam urosnąć do kilku tysięcy linii kodu. Dzięki podziałowi możemy tą ilość podzielić.

Moja implementacja tego wzorca cały czas ewoluuje. Oparłem ją na wzorcach, które zdefiniował Martin Fowler. Passive View definiuje widok jako reprezentację graficzną, która jest „głupia” jeżeli chodzi o obsługę komend i do tego celu wykorzystuje prezentera. Supervising Controller określa prezentera jako sterownik, który odbiera komendy od widoku i zawiaduje całą logiką. Moją implementację staram się cały czas ulepszać. Jeżeli jeszcze tego nie robicie to zachęcam Was do eksperymentów i zaadaptowania tego wzorca. Zapewniam, że włożony w to wysiłek na pewno się zwróci. Na początku może się to wydawać niepotrzebnie skomplikowane i na wyrost, ale wraz z wzrostem systemu dostrzeżecie zalety.

Opublikowane 12 października 2008 15:48 przez nuwanda

Powiadamianie o komentarzach

Jeżeli chciałbyś otrzymywać email gdy ta wypowiedź zostanie zaktualizowana, to zarejestruj się tutaj

Subskrybuj komentarze za pomocą RSS

Komentarze:

# re: M-V-P Twoim przyjacielem

15 października 2008 08:36 by bszafko

bardzo fajny tekst

podpisuje się rękami i nogami pod tym :)

Co o tym myślisz?

(wymagane) 
wymagane 
(wymagane) 

  
Wprowadź kod: (wymagane)