Czym jest MVC?
Aplikacje składają się zazwyczaj z wielu różnych elementów, które muszą ze sobą jakoś współpracować. Odwiecznym problemem jest znalezienie sposobu na taką organizację poszczególnych komponentów, by nie mieszać ze sobą fragmentów kodu odpowiedzialnych za dwie różne rzeczy oraz aby dało się nimi łatwo zarządzać. Przyjrzyjmy się typowej aplikacji tworzonej przez początkującego programistę PHP:
<?php require('./costam.php'); polaczMniezBaza(); if($_SESSION['logged'] == true) { echo '<ul>'; // tak w ogole to skad sie biora ludzie wciaz uzywajacy funkcji // mysql_()? $r = mysql_query('SELECT * FROM costam'); while($row = mysql_fetch_assoc($r)) { echo '<li>'.$row['name'].'</li>'; } echo '</ul>'; }
Kod ten jest prosty i szybki dzięki trzymaniu się zasady "po trupach do celu". Początkujący programista tworzy taki kod, ponieważ lepszego nie umie, natomiast zaawansowany programista tworzy taki kod w malutkich skrypcikach, gdzie nie ma sensu bawić się w jakieś dziwaczne podziały. Jednak jeśli wyobrazimy sobie, że jest on częścią jakiejś bardzo dużej aplikacji, jego obecna budowa jest niesamowicie problematyczna. Popatrzmy, co udało nam się wymieszać w tych kilku linijkach:
- warstwa wizualna, czyli kod HTML,
- logika aplikacji, czyli komunikacja z bazą danych,
- system autoryzacji.
W dużych i stale rozwijanych aplikacjach nie ma mowy o pisaniu w ten sposób. Gdzieś pojawi się błąd wymagający zmiany fragmentu jakiegoś mechanizmu, gdzieś trzeba będzie dodać nową funkcjonalność - dla nas oznacza to wielogodzinne siedzenie i poprawianie kawałek po kawałku setek malutkich plików-spaghetti, a wszystko przez to, że nawet nam się nie chciało poopakowywać tego w głupie funkcje.
Jako programiści możemy oczywiście siąść i wymyślić jakiś autorski sposób podzielenia tego wszystkiego na klasy, ale minie bardzo dużo czasu, zanim uda nam się opracować od zera coś naprawdę dobrego, nie wspominając już o odkryciu wszystkich właściwości zaproponowanego rozwiązania, gdyż te czasami wychodzą dopiero "w praniu". Dlatego istnieją wzorce projektowe, czyli sprawdzone przepisy na osiągnięcie określonych rezultatów. My chcemy mieć elegancką i logiczną strukturę aplikacji, a dostarczyć ją nam może wzorzec Model-View-Controller. Zakłada on podział aplikacji na trzy główne elementy:
- Model - zawiera całą logikę biznesową aplikacji, czyli operacje na danych, które mogą być zgromadzone w plikach lub w bazie danych.
- Widok - zawiera cały kod odpowiedzialny za prezentację wyników i innych rzeczy użytkownikowi.
- Kontroler - nadzoruje proces wykonywania aplikacji i kojarzy ze sobą modele i widoki.
Gdybyśmy chcieli użyć MVC w naszym przykładzie powyżej, pobranie wierszy z bazy umieścilibyśmy w modelu, wyświetlenie tychże wierszy w widoku, zaś sprawdzenie czy użytkownik jest zalogowany - w kontrolerze. Kontroler dodatkowo musiałby wybrać odpowiedni model oraz widok. Istota MVC polega na tym, żeby nie mieszać odpowiedzialności poszczególnych warstw, czyli np. nie umieszczać elementów odpowiedzialnych za wyświetlanie bezpośrednio w kontrolerze. Zależności między elementami przedstawia poniższy diagram:
Kontroler posiada wskazanie na model i widok z tego prostego powodu, że tworzy obie części i musi je skonfigurować do pracy ze sobą. Ponadto widok powinien posiadać wskazanie na model, aby móc pobierać z niego dane. Na tę kwestię pragnę zwrócić szczególną uwagę, ponieważ tu leży jedno z największych nieporozumień dotyczących wzorca MVC. W 99% tutoriali możemy napotkać stwierdzenie, że dane z modelu pobiera kontroler, a następnie przekazuje je do widoku. Nic bardziej mylnego - taka praktyka jest oczywiście możliwa, ale we wzorcu, który nazywa się Model-View-Presenter. My tymczasem omawiamy Model-View-Controller, gdzie widok pobiera sobie dane bezpośrednio z modelu poprzez jakiś dobrze zdefiniowany interfejs.
W aplikacjach okienkowych wykorzystywany jest także wzorzec Obserwator, dzięki któremu model może powiadomić kontroler o samoistnej zmianie stanu. Przykładowo, zlecamy modelowi skopiowanie jakiegoś pliku. Kontroler powinien zareagować na sygnał, że kopiowanie się zakończyło, dlatego rejestruje się w modelu jako obserwator i dzięki temu dostaje od niego powiadomienie, gdy proces się zakończy. Mamy tutaj do czynienia z tzw. aktywnym MVC, który jednak nie ma żadnego praktycznego zastosowania w sieci WWW. Protokół HTTP działa na zasadzie żądanie - odpowiedź, gdzie cała procedura wygląda następująco:
- Do serwera przychodzi żądanie HTTP.
- Serwer odpala skrypt PHP.
- Skrypt PHP odpala kontroler.
- Kontroler patrzy, co od niego chcemy w żądaniu.
- Kontroler odpala żądaną akcję.
- Akcja wybiera model oraz widok.
- Widok pobiera dane z modelu.
- Widok generuje odpowiedź (np. kod HTML).
- Widok przekazuje odpowiedź HTTP do serwera.
Nie ma tu miejsca na żadną zwrotną odpowiedź z modelu do kontrolera, dlatego w aplikacjach WWW obserwator nie będzie nam potrzebny. Taki wariant wzorca nazywamy pasywnym MVC, albo MVC z pasywnym modelem.
Gdy wiemy już, jak MVC osadzony jest w procesie generowania odpowiedzi HTTP, pora przyjrzeć się bliżej poszczególnym jego komponentom.
Model
Warstwa modelu jest najprostsza do zbudowania, a co ważniejsze - może funkcjonować zupełnie w oderwaniu od całej reszty aplikacji. Zadaniem modelu jest zarządzanie logiką biznesową aplikacji, tj. wykonywanie operacji na bazie danych, na plikach, usługach web oraz innych bytach, gdzie przechowujemy jakieś istotne informacje. Istotne jest, że aplikacja korzystająca z takiego modelu nie powinna być świadoma czy odbierane właśnie dane pochodzą z bazy czy z jakiegoś innego źródła. Wszystko powinno być ukryte za pewnym abstrakcyjnym interfejsem, przy czym mamy pozostawioną pełną swobodę co do jego kształtu.
Zbudujmy przykładowy model:
<?php class UserModel { /** * Zwraca listę użytkowników w systemie. */ public function listUsers() { $pdo = Registry::get('pdo'); $stmt = $pdo->query('SELECT * FROM users'); $list = array(); while($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $list[] = $row; } $stmt->closeCursor(); return $list; } // end listUsers(); } // end UserModel;
Proste, prawda? Jeśli w jakimś miejscu aplikacji będziemy chcieli teraz pobrać listę użytkowników, wystarczy że stworzymy obiekt klasy UserModel (albo weźmiemy już istniejący) i wywołamy metodę listUsers(). Korzyści z takiej separacji:
- Jeśli jakaś część logiki nie jest jeszcze zaimplementowana, możemy wstawić do modelu metody-atrapy, które będą wypluwać statyczne tablice.
- Elastyczność - zmiana struktury bazy danych lub nawet źródła danych (np. przejście z plików na bazę) nie wymaga modyfikacji reszty aplikacji.
- Łatwość testowania - możemy testować warstwę logiki w całkowitym oderwaniu od reszty systemu.
- Mniej zależności - warstwa logiki może powstawać niezależnie od reszty aplikacji, a nawet być używana w innych projektach bez żadnych zmian.
W tej pozornej swobodzie kryje się jednak wiele kruczków. W rzeczywistości sama obecność tych zalet zależy od tego, jak zaprojektujemy interfejs warstwy modelu. Jedna głupia decyzja może nas sporo kosztować, dlatego nie podejmujmy ich pochopnie. Pierwsza głupia decyzja może zostać podjęta, jeśli korzystamy z bibliotek Object-Relational Mapper (ORM). Są one zazwyczaj na tyle sprytne, by zrodzić w nas pokusę pt. "po co mam jeszcze stosować jakąś warstwę abstrakcji, skoro mogę wykorzystać bezpośrednio obiekty ORM-a w roli modelu. Pokusie tej uległo 99% twórców frameworków, a efekt jest taki, że nie da się w nich zrobić modelu korzystającego z czegoś innego, niż baza danych, a przy okazji złamane zostaje jedno z kluczowych założeń wzorca MVC. Zapamiętajmy sobie raz na zawsze: ORM to nie model, model to nie ORM. Model może korzystać z ORM-a, ale ORM nie może zastąpić nam modelu.
Z naszych modeli musi korzystać reszta aplikacji. Zauważmy, że byłoby straszliwą niegospodarnością nadawanie metodom wyszukanych, unikalnych nazw w stylu listUsersById(), listRolesById(), ponieważ do wywołania każdej takiej metody musielibyśmy napisać osobny kod (a w szczególności - osobny widok). Dlatego zastanówmy się, jak sprawić, by jeden widok mógł obsłużyć kilkanaście różnych modeli bez zajmowania się szczegółami tego, co reprezentują. Oczywiście taki widok będzie musiał znać pewien interfejs modelu, a to prowadzi nas do ciekawego wniosku, że kluczem do budowy udanej warstwy modelu jest zestaw uniwersalnych interfejsów komunikacyjnych. Zróbmy sobie taki jeden:
<?php interface Listable { public function listItems(); public function getColumnHeaders(); } // end Listable; interface Paginable extends Countable { public function setPage($page); } // end Paginable; interface SingleItemAccess { public function getRow($id); public function getRowDescription(); } // end SingleItemAccess;
Implementując różne kombinacje tych interfejsów w naszych modelach możemy definiować ich rozmaite właściwości. Jeśli chcemy utworzyć model do wyświetlenia listy elementów ze stronicowaniem, wystarczy że zaimplementujemy w nim interfejsy Listable oraz Sortable. Co więcej, ich obecność może być sygnałem dla warstwy widoku: "aha, mam tutaj taki model, który implementuje Paginable, zatem wyświetlę mu stronicowanie". Dobrze skonstruowane interfejsy mogą dać nam ekstremalną wręcz elastyczność, a zależy ona jedynie od naszej pomysłowości oraz umiejętności przekucia naszych pomysłów w kod. W praktyce może okazać się, że utworzenie aplikacji sprowadzi się głównie do napisania warstwy modelu.
Tuż po postawieniu pierwszych kroków i wychyleniu głowy poza świat tutoriali w kierunku prawdziwych aplikacji możemy zadać sobie następujące pytania:
- Czy kontroler ma prawo pobierać dane z modelu?
- Jak przekazać pewne ustawienia do modelu?
- Jak pogodzić modele z kontrolą uprawnień?
- Gdzie konfigurować formularze?
Na pierwsze pytanie odpowiedź jest bardzo prosta: kontroler ma takie samo prawo do wywołania metod modelu, jak widok. Przecież nie możemy wykluczyć, że jakieś konkretne informacje będą potrzebne właśnie kontrolerowi. Należy jedynie pamiętać, by nie pobierać tych danych jedynie po to, by chwilę później wepchnąć je do widoku. Oczywiście skoro możemy wywoływać metody modelu, możemy też utworzyć w nim pewne metody do przekazywania z żądania HTTP pewnych ustawień. Na trzecie pytanie odpowiedziałem sobie następująco: modele powinny być w miarę możliwości niezależne od reszty aplikacji. Jeśli zwiążemy je na sztywno z systemem ACL, ich testowanie będzie utrudnione. Co więcej, może okazać się, że metoda X jest używana w kilku różnych miejscach i w każdym z nich wymagania co do dostępu są inne. Dlatego kontrolą uprawnień powinien zająć się kontroler i jedynie przekazać wyniki modelowi, np. w formie listy kluczy z wartościami true lub false. Jeśli model musi rozpatrywać dużo warunkowych wyborów, można odwrócić sterowanie - niech kontroler ma metodę mapującą pytania modelu na odpowiednie klucze w ACL i niech ma pewien interfejs z metodą isAllowed(). Niech się wstrzyknie sam do modelu, dzięki czemu model będzie mógł odpytywać system ACL dynamicznie, ale nie bezpośrednio.
Formularze to najbardziej złożony problem, ponieważ proces ich obróbki angażuje wszystkie trzy warstwy MVC:
- Model decyduje o zestawie pól oraz regułach poprawności danych.
- Kontroler zajmuje się sterowaniem procesem przetwarzania formularza.
- Widok zajmuje się wyświetleniem formularza, czyli ubraniu pól w kontrolki i inne wizualne atrybuty.
Prawdę mówiąc miałem (i dalej mam) niezłą zagwozdkę z tym. Póki co testuję połączenie, w której mamy obiekt formularza z osobno zdefiniowanymi regułami poprawności danych oraz wizualnym (w oddzielnych metodach). Jest on zarządzany w kontrolerze, może tam też być stworzony, albo pobrany z modelu. Jak wszystko poszło dobrze, kontroler oddaje nam sterowanie, byśmy mogli coś z danymi zrobić, a jak nie - jedziemy do widoku.
Widok
Warstwa widoku jest już nieco bardziej skomplikowana, ponieważ musimy mieć możliwość wstrzykiwania do niej modeli. Dlatego zbudujmy sobie bazową klasę widoku:
<?php class View { private $_models = array(); private $_data = array(); public function __set($name, $value) { $this->_data[$name] = $value; } // end __set(); public function __get($name) { if(!isset($this->_data[$name])) { return null; } return $this->_data[$name]; } // end __get(); public function get($name, $default = null) { if(!isset($this->_data[$name])) { return $default; } return $this->_data[$name]; } // end get(); public function addModel($name, $object) { $this->_models[(string) $name] = $object; } // end addModel(); public function hasModel($name) { return isset($this->_models[(string) $name]); } // end hasModel(); public function getModel($name, $contract = null) { if(!isset($this->_models[(string) $name])) { throw new Exception('The model '.$name.' is not defined.'); } if($contract !== null) { if(!is_a($this->_models[(string) $name], $contract)) { throw new Exception('Model contract '.$contract.' is not satisfied for '.$name); } } return $this->_models[(string) $name]; } // end getModel(); abstract public function display(); } // end View;
Cały kod odpowiedzialny za wyświetlenie czegoś znajdzie się w metodzie display(), która musi zostać zaimplementowana przez określoną klasę potomną. Oprócz tego do dyspozycji jest interfejs do zarządzania modelami. Dla kontrolera przeznaczona jest metoda addModel(), zaś dla widoku - hasModel() oraz getModel(). Zastosowaliśmy tutaj elementy programowania kontraktowego. Kontrakt to pewna umowa, że z danym interfejsem można komunikować się na określony sposób i że uzyskamy określony rodzaj wyniku. Jest to naturalne rozszerzenie idei dobrze zdefiniowanych interfejsów z warstwy modelu. Określone rodzaje widoków mogą współpracować z określonymi interfejsami, dlatego pobierając model dostarczony przez kontroler warto sprawdzić czy faktycznie implementuje on interfejs, który widok potrafi obsłużyć. Jeśli podstawowy kontrakt nie jest spełniony, rzucamy wyjątek. Oczywiście mogą też istnieć kontrakty opcjonalne takie, jak wspominany już przykład ze stronicowaniem, ale je możemy już sprawdzić operatorem instanceof. Do interfejsu widoku zaliczamy też settery i gettery, dzięki którym kontroler może przekazać do widoku różne parametry.
Przy implementowaniu warstwy widoku także możemy popełnić głupią decyzję projektową. Ma ona podobne źródło, jak problem ORM-ów w warstwie modelu, ale jest chyba nawet bardziej niebezpieczna. O ile bowiem zdecydowana większość aplikacji WWW do składowania danych wykorzystuje wyłącznie bazę, to w warstwie widoku zdecydowana większość aplikacji generuje coś więcej niż tylko kod HTML. A jeśli pokusimy się, by w ramach źle pojmowanej prostoty zrównać widok z systemem szablonów, nagle okaże się, że połowy z tych pozostałych rzeczy nie będziemy w stanie odwzorować jako widok! Prosty przykład to dokument do wydruku w formacie PDF. Z formalnego punktu widzenia jego generacją powinien zająć się właśnie widok. PDF jest wprawdzie formatem tekstowym, ale do jego tworzenia wykorzystuje się specjalne biblioteki, a nie szablony! Nagle okazuje się, że cały ten kod musi wylądować w kontrolerze, bo nie ma co z nim zrobić.
Kolejny wynikający z tego problem to nieuwzględnienie różnych elementów sterowania prezentacją jako części widoku. Na potrzeby tego wpisu nazwę je logiką prezentacji, a jej przykładami może być np. stronicowanie i sortowanie tabelki po kolumnach. Logiki prezentacji zasadniczo nie osadza się w szablonach, ponieważ te mają inne zastosowanie, a kod wynikowy byłby strasznie nieczytelny. Zauważmy, że takie sortowanie ma sens jedynie w przypadku generowania danych w formacie HTML. Zrównując widok z szablonami musimy kod do jego obsługi umieścić w kontrolerze, a tym samym uzależnić go od konkretnego sposobu prezentacji, co w MVC nie powinno mieć miejsca.
Poniżej przedstawiam przykładowy widok renderujący szablon OPT:
<?php class View_Grid extends View { public function display() { $layout = Registry::get('layout'); $config = Registry::get('config'); $view = new Opt_View('grid.tpl'); $model = $this->getModel('list', 'Listable'); $view->headers = $model->getColumnHeaders(); // Jesli model wspiera stronicowanie, to wyswietlmy mu // stronicowanie if($model instanceof Paginable) { $paginator = new Paginator($model->count(), $config->itemsPerPage, $this->get('pageNumber', 1)); $view->paginator = $paginator; $model->setPage($paginator->getPage()); } $view->items = $model->listItems(); $layout->appendView('content', $view); } // end display(); } // end View_Grid;
Jest to widok uniwersalny, który może być użyty z różnymi kontrolerami i różnymi widokami. Ukazuje on całą potęgę drzemiącą w prawdziwym MVC. Mamy jedną klasę odpowiedzialną za wyświetlanie listy elementów, którą wykorzystujemy w całej aplikacji. Jeśli stwierdzimy, że nasze tabelki z wierszami potrzebują jakiegoś kolejnego wyszukanego elementu prezentacyjnego, tworzymy dla nich interfejs modelu i dopisujemy odpowiedni kawałek kodu do tej jednej klasy. Od tej pory dla każdego modelu, który zaimplementuje nowy interfejs, widok wyświetli określony element. Nie musimy poprawiać dziesiątek plików, co gorsza - dziesiątek kontrolerów, jak to ma miejsce w wielu frameworkach, o ile sobie tam w jakiś sposób tego nie obudujemy własnoręcznie. Mamy kod odpowiedzialny za prezentację niezależy zarówno od tego, co, jak i gdzie wyświetlamy.
Może się tutaj pojawić tylko jedno pytanie: jak zarządzać widokami, gdy kontroler może odpalić kilka akcji? Moja odpowiedź brzmi następująco: podzieliłem warstwę prezentacji na dwie mniejsze części: broker oraz właściwe widoki. Broker to pewien menedżer decydujący o tym, jaki rodzaj wyjścia zostanie wysłany: HTML, PDF, XML, plik do ściągnięcia czy coś jeszcze innego. Brokerem może być np. dobrze znany z Zend Frameworka komponent Zend_Layout. Odbiera on wygenerowane przez widoki kawałki kodu HTML, osadza w layoucie i wysyła do przeglądarki. Dzięki temu możliwe jest wykonanie kilku akcji, przy czym każda z nich odpala własny widok. W danym żądaniu HTTP może działać tylko jeden broker. Jeśli któryś widok próbuje wgrać dane nierozpoznawane przez broker (np. generowanie pliku PDF, gdy inna akcja generuje HTML), generowany jest wyjątek. Co ważniejsze, broker tworzony jest dopiero w momencie, gdy pojawi się pierwszy widok. Dzięki temu unikniemy niepotrzebnego inicjowania menedżera layoutów, gdy chcemy wysłać plik PDF albo odpowiedź AJAX w formacie XML.
Kontroler
Dochodzimy do meritum, czyli do kontrolera. Zadanie kontrolera to spojenie dwóch poprzednich warstw i sprawienie, by wszystko zaczęło działać. Kontroler dostaje na wejściu informacje o bieżącym żądaniu HTTP. Na ich podstawie wybiera jakąś akcję do wykonania. W akcji wybierany jest widok oraz model, które produkują jakiś wynik. Kontroler odbiera go i odsyła do przeglądarki. Dla celów czytelności odwołam się do najdebilniejszego przykładu kontrolera na świecie:
<?php interface Controller { public function dispatch(Request $request, Response $response); } // end Controller;
Request oraz Response to dwie klasy reprezentujące odpowiednio żądanie i odpowiedź HTTP. Z Request możemy odczytać sobie np. parametry wywołania naszego skryptu czy dowiedzieć się, jaka metoda HTTP została użyta. To, co zostanie wygenerowane przez widoki oraz nagłówki ustawiane wszędzie, gdzie tego potrzeba, trafiają do obiektu Response. Z niego posyłamy je dalej w świat. Poniżej przedstawiam prostą implementację kontrolera:
<?php class StupidController implements Controller { public function dispatch(Request $request, Response $response) { if(method_exists($this, $request->getParam('action', 'index').'Action')) { $method = $request->getParam('action', 'index').'Action'; $view = $this->$method(); $view->display(); $broker = Registry::get('broker'); $broker->display($response); } else { throw new Error404_Exception; } } // end dispatch(); public function usersAction() { $model = new UserModel; $view = new View_Grid; $view->addModel('list', $model); return $view; } // end usersAction(); public function rolesAction() { $model = new RoleModel; $view = new View_Grid; $view->addModel('list', $model); return $view; } // end rolesAction(); } // end StupidController;
W tym przypadku mapujemy sobie parametr action na nazwę odpowiedniej metody, tj. wywołując index.php?action=users, odpalimy metodę usersAction(), zaś dla wartości roles odpalimy rolesAction(). Zauważmy, że kody poszczególnych akcji są niezwykle krótkie. Mamy tutaj właściwie jedynie wybór modelu, wybór widoku i skomunikowanie ich ze sobą. Widok jest zwracany i odpalany przez metodę dispatch(), która następnie dodatkowo odpala broker widoku.
W typowej aplikacji WWW stosuje się gotowe, dużo bardziej zaawansowane kontrolery. Najpopularniejsza implementacja to kontrolery dwupoziomowe. Tworzymy sobie pliki w stylu IndexController, UsersController, zaś w nich implementujemy poszczególne akcje. Jako argument podajemy nazwę "kontrolera" (właściwie bardziej pasuje tu określenie grupy akcji) oraz konkretnej akcji. Zadanie kontrolera to odnalezienie odpowiedniej grupy oraz odpowiedniej akcji i doprowadzenie do ich uruchomienia. Taki podział to kompromis między ilością możliwych akcji, a ich rozbiciem na mniejsze pliki. Z jednej strony, mając zgrupowanych kilka tematycznie powiązanych akcji w jednej klasie, łatwiej nimi zarządzać oraz łatwiej ogarnąć taką ilość plików, a z drugiej wywołując jedną, jedyną akcję, musimy załadować też kod pozostałych z tej samej grupy.
Większość frameworków kończy swoje zadanie w tym miejscu, udostępniając tylko tę jedną implementację i nic więcej, jednak do pewnych zastosowań jest ona mocno niewygodna. Do aplikacji w stylu bloga o wiele lepsza byłaby architektura jednopoziomowa, gdzie każda akcja stanowi samodzielny i niezależny byt. Z kolei w CMS-ie przydałby się kontroler, który definicję określonej strony wczytywałby z ustawień zapisanych w bazie danych. Być może inni programiści znajdą jeszcze więcej przykładów, gdzie można by zastosować jeszcze inne podejście.
Jak wspomnieliśmy, kontroler nie powinien zajmować się sprawami logiki biznesowej, które powinny być delegowane do modelu, ani prezentacji, delegowanych do widoku. Mogłoby się wydawać, że nie zostaje mu już żadne ciekawe zadanie. Nic bardziej mylnego. Kod interpretujący argumenty wywołania może być niekiedy bardzo skomplikowany i liczyć sobie dziesiątki linijek kodu. Ponadto to właśnie tutaj możemy umieścić wszelkie sprawdzanie autoryzacji oraz kontrolę uprawnień. Można zadać sobie pytanie czy kontroler musi w ogóle korzystać z warstwy widoku. Odpowiedź brzmi: nie. Sama specyfikacja wzorca też nic nie mówi, że widok musi być bezwzględnie wywołany, "bo cośtam", a w protokole HTTP mamy coś takiego, jak przekierowanie. Nic nie stoi na przeszkodzie, aby kontroler wykonał jakąś operację zmiany danych na modelu i natychmiast wygenerował nagłówek Location.
MVC kontra frameworki
Nie ukrywam, że większość podanych powyżej informacji zupełnie nie przyda się podczas korzystania z typowego frameworka WWW z bardzo prostego powodu. Praktycznie żaden framework tak naprawdę MVC nie implementuje. Gdy kilka miesięcy temu pisałem o MVC przy okazji omawiania Yii, z powodu takiego właśnie stwierdzenia ktoś zarzucił mi w komentarzach, że "nie rozumiem idei wzorców projektowych" (wywołując przy tym sporą wesołość u mnie na uczelni :)). Z tym stwierdzeniem nie mogę się zgodzić. Nie krytykuję tutaj implementacji, nie krytykuję konkretnego rozwiązania. Krytykuję wypaczanie idei wzorców projektowych, czyli nazywanie MVC czegoś, co MVC definitywnie nie jest w myśl podanej we wstępie zasady, że nazwanie jednej klasy obserwatorem, a drugiej obserwowanym nie daje nam jeszcze wzorca "Obserwator". Bez względu na to jak mądry/sławny jest gość, który takie nazwy sobie zastosował. Istotne są jeszcze definiowane przez wzorzec pewne interakcje, a dopiero na nich buduje się mniej lub bardziej udaną implementację. W przypadku frameworków interakcje reprezentowane przez ich implementacje pasują do wzorca Model-View-Presenter, którego założeniami jest właśnie likwidacja bezpośredniej komunikacji między modelem i widokiem oraz uproszczenie tych dwóch warstw opisane w tym wpisie jako "głupie decyzje projektowe". Były one głupie jedynie z punktu widzenia MVC, natomiast normalnie nie da się jednoznacznie powiedzieć, że któryś z tych wzorców jest lepszy lub gorszy. MVC był historycznie pierwszy, dlatego ktoś, kto stworzył MVP i nazwał go tak, musiał mieć określony powód, by to zrobić. Dopiero później pojawił się Ruby On Rails, w którym MVP został "przechrzczony" na MVC i się zaczęło.
Eksperymentalna implementacja Trinity
Jedyna działająca implementacja MVC znajduje się obecnie w CMS-ie Joomla (i jest to jedyna dobra rzecz, jaką można powiedzieć o jej kodzie). Poza tym cały świat WWW ostro trzyma się wzorca MVP. Dlatego największą przeszkodą w stosowaniu, a nawet przetestowaniu MVC jest brak jakiejkolwiek sensownej implementacji. Część koncepcji omówionych w tym wpisie opracowałem podczas realizacji projektu na wspomnianej Joomli, gdzie de facto musiałem stworzyć własny mini-framework, by poradzić sobie z absurdami tego CMS-a i jak najbardziej się od nich odseparować. Jednak dalsza praca na czymś takim byłaby masochizmem, dlatego później zajmowałem się głównie teorią. Przyszła jednak pora, że ciekawość pchnęła mnie do rozpoczęcia prac nad eksperymentalną, samodzielną implementacją, którą roboczo nazwałem Trinity. Pierwsze linijki kodu powstały już podczas pobytu na PHPCon, zaś od kilku dni projekt znajduje się na Githubie, gdzie można śledzić postępy.
W Trinity testuję następujące koncepcje:
- implementacja MVC,
- programowanie zdarzeniowe (pozdrowienia dla Damiana Tylczyńskiego :))
- leniwe zarządzanie zależnościami oraz ich wstrzykiwanie,
- warstwowa budowa systemu umożliwiająca prostszą wymianę niektórych elementów,
- rezygnacja z implementowania każdej głupoty we własnym zakresie na rzecz wykorzystania zewnętrznych bibliotek,
- elementy programowania kontraktowego,
- kratownicowy podział aplikacji na moduły.
Repozytorium dostępne jest tutaj i zawiera debilną, przykładową aplikację. Do działania wymagane są:
- PHP 5.3+
- Doctrine 2-BETA
- Open Power Libs 2.1-dev (github)
- Open Power Template 2.1-dev (github)
- Open Power Classes 2.1-dev (github)
- Open Power Forms 2.1-dev (github)
- Zalecany APC lub inny akcelerator i cache
Należy wszystkie biblioteki wgrać do katalogu /libs i w /www/index.php skonfigurować odpowiednio autoloadery.
Kilka dotychczasowych wniosków:
- Udało się opracować sensowną sekwencję startową dla aplikacji MVC.
- Stosowane we frameworkach nazewnictwo jest dostosowane do sposobu działania MVP i niezbyt dobrze oddaje funkcjonalność elementów związanych z działaniem MVC. Dlatego czytając cokolwiek należy zapomnieć o terminologii tam stosowanej, ponieważ może to prowadzić do nieporozumień. To samo dotyczy praktyk programistycznych.
- W przypadku bardzo prostych akcji MVC wymaga napisania nieco większej ilości kodu niż MVP do zrobienia tego samego (akcja + model + widok + szablon kontra akcja + szablon + użycie ORM-a).
- MVC jest bezkonkurencyjny, jeśli dany widok może być użyty więcej niż raz.
- Niskopoziomowe tworzenie kontrolerów oraz brokerów widoku wymaga przyzwyczajenia.
- Wstrzykiwanie zależności oraz leniwe zarządzanie nimi jest wygodne, ale włączenie w łańcuch niepowiązanej z niczym usługi jest dość problematyczne.
- Wstrzykiwanie zależności oraz ogólnie unikanie różnych globalnych rejestrów (np. Zend_Registry) może dość mocno wydłużyć kod pozyskiwania jakiegoś obiektu, a także wymusza stosowanie w obiektach mnóstwa kodu do odbierania wstrzykiwanych obiektów.
- Kratownicowe moduły są genialne i aż dziw, że nikt jeszcze na to nie wpadł.
- Własny kontener IoC fajnie się pisze, dopóki nie trzeba robić automatycznej wstrzykiwarki zależności na podstawie dostarczonych definicji.
- W Open Power Template 3.0 klasa Opt_View musi mieć zupełnie inną nazwę :)
- Dopiero tworząc framework człowiek zdaje sobie w pełni sprawę, z ilu rzeczy zwalnia go użycie "gotowca"...
- Z drugiej strony przerobienie większości istniejących frameworków na MVC tak, by nie było żadnych śmieci, wymaga niezwykle radykalnych zmian w ich budowie.
- Przestrzenie nazw przy braku czujności programisty potrafią spowodować niesamowity sajgon w kodzie. Część edytorów nienajlepiej obsługuje wtedy podpowiadanie kodu (NetBeans), a ponadto nie możemy już zrobić sobie pliku List.php i w nim klasy List w przestrzeni Foo, ponieważ... list to słowo kluczowe PHP.
- Użycie zewnętrznych bibliotek sprawia, że niektóre komponenty się niepotrzebnie dublują. Doctrine 2 posiada menedżer zdarzeń, Trinity też posiada menedżer zdarzeń. Ich interfejsy są niemal takie same, poza dwoma drobnymi szczegółami: menedżer z Doctrine nie wspiera funkcji anonimowych i wymaga, by lista argumentów zdarzenia była... obiektem specjalnej klasy. Którą trzeba samodzielnie rozszerzyć. Życzę powodzenia, jak w systemie jest 500 różnych możliwych zdarzeń.
Zakończenie
Celem tego wpisu było wniesienie do suchej gadaniny MVC kontra MVP trochę namacalnego kodu, aby można było w praktyce zobaczyć, jak to wszystko wygląda i wyrobić sobie jakąś opinię na ten temat. Jeszcze raz podkreślam, że w tym wszystkim chodzi mi głównie o to, by widząc skrót "MVC" można było znaleźć pod nim faktycznie MVC, a nie krasnoludki, abstrahując zupełnie od umiejętności krasnoludków w zakresie tworzenia aplikacji WWW. Taka jest idea wzorców projektowych i miło byłoby, gdyby MVC przestał być wyjątkiem ją potwierdzającym. Przy okazji mam nadzieję zebrać nieco uwag dotyczących umiejscowienia różnych elementów w MVC, ponieważ - co tu dużo mówić - przy braku sensownego punktu odniesienia trzeba wszystko wymyślać samemu, a jak to przy takim wymyślaniu bywa - czasami popełnia się błędy.






Napisał Zyx w sobotę, 31 lipca 2010 o 20:29
Możesz to zrobić na dwa sposoby: pierwsze to zbudować widok, który jednocześnie będzie potrafił wyświetlić i tabelkę, i newsy. Oczywiście nie jest to zbyt elastyczne i może zmusić Cię do wynajdowania koła na nowo. Dlatego zerknij sobie na wpis o cegłach, który dodaje do Trinity koncepcje zbliżone do wzorca HMVC. Robisz sobie dwie cegły: jedną do tabelki, drugą do newsów, a następnie w akcji kontrolera odpalasz najpierw pierwszą, a potem drugą cegłę. Każda cegła posiada własny widok i każda cegła konfiguruje go niezależnie. Trinity potrafi wyświetlić w jednym żądaniu szablony z więcej, niż jednego widoku na zasadzie podobnej, jak to ma miejsce w ZF. Jedyna różnica polega na tym, że tam akcje uruchamiane są sekwencyjnie, a tu - hierarchicznie.
Do dopracowania pozostaje tu jeszcze kwestia generowania nawigacji, ale ta sprawa jest u mnie aktualnie na tapecie, więc jak tylko rozwiążę wszystkie problemy i to zaimplementuję, nie omieszkam podzielić się wynikami.