Dobre praktyki programowania w CakePHP #4

O podobnych kwestiach pisałem już przy okazji wpisu #2 z tej serii. Ale warto jeszcze raz przypomnieć o tym, że tak jak zasady projektowania obiektowego tak należy dokładnie zrozumieć co oznacza podział aplikacji na warstwy MVC.

W tym wpisie skupię się na warstwie modelu.
Jest ona czasem nazywana warstwą biznesową aplikacji- i nie jest to przypadek. Dzieje się tak dlatego, że w niej zawarte są (albo powinny być) reguły biznesowe, które dany system realizuje. Przejdę jak najszybciej do przykładu, który to obrazuje.

Wyobraź sobie system zarządzania firmą wypożyczającą samochody. Cake wypiekł Ci CRUD dla modelu Cars i Costs (koszty generowane przez te samochody). Teraz klient informuje Cię o pewnej zasadzie, która brzmi:
“Każdy koszt można powiązać z dowolną ilością samochodów. Koszt może nie być powiązany z żadnym samochodem (koszt ogólny). Dane powiązanie koszt-samochód jest opatrzone konkretną kwotą (kwota powiązania). Jeśli koszt jest powiązany z jednym lub więcej samochodami, to suma kwoty ich powiązań musi się równać wartości kosztu. Czyli koszt musi być całkowicie rozdzielony na samochody, lub wcale.
Na przykładzie: mogę dodać koszt ‘naprawa po wypadku’, który powiążę z jednym samochodem na pełną kwotę. Mogę dodać koszt ‘ubezpieczenie za rok 2008’, które rozdzielę po równo między wszystkimi samochodami i mogę dodać ‘spinacze do biura’, którego nie powiąże z żadnym samochodem”.
To co właśnie usłyszałeś to reguła biznesowa, którą Twoja aplikacja musi obsłużyć.

Od razu oczywiście zabierasz się ochoczo za zdefiniowanie relacji wiele do wielu (HABTM) w modelach Cost i Car. Jeśli uważasz, że to wszystko i modele już będą jak zwykle służyć do wykonywania selectów na bazie- to sygnał, że możesz jeszcze nie rozumieć idei MVC, więc czytaj dalej.

Zabierasz się za implementowanie funkcjonalności. Bez większego zagłębiania się w szczegóły zbudujesz widok, w którym wybierzesz dla danego kosztu ileś samochodów z listy, wypełnisz ich kwoty i po kliknięciu zapisz poleci wszystko do kontrolera, a w nim będziesz miał 4 (o cztery za dużo) foreachów, którymi będziesz sprawdzał, czy suma się zgadza, a jeśli tak – pozwolisz na zapisz, bla, bla, bla. Jesteś zadowolony.

Jednak za miesiąc przychodzi klient i mówi: “fajne jest to przypisywanie kosztów do samochodów, ale ja chciałbym, żeby móc do kosztu przypisać samochody tylko na część kwoty”.
Jako porządny podwykonawca zgadzasz się, dajesz nura w kontroler i rozdmuchujesz go o pięć nowych ifów i trzy foreache, żeby obsłużyć wszystkie niuanse, które właśnie się pojawiły.

Ale licho… ekhm, klient nie śpi: “Skoro można przypisywać samochody do kosztów nie w całości – to chciałbym móc w widoku samochodu możliwość wybrania kosztów i je do niego przypisać”.
I co teraz robisz?

Jeśli zabierasz się za przeniesienie mechanizmów z kontrolera Costs do modelu Cost – może nie pójdziesz jeszcze do piekła. Jeśli zapaliła Ci się czerwona lampka, ale nie wiesz o co chodzi – czytaj dalej. Jeśli zaczynasz kopiować wspomniane mechanizmy do kontrolera Cars – jesteś w ciemnej du… hmm piwnicy. Dlaczego?

To, że łamiesz zasadę MVC to już powinieneś czuć przez skórę. Możesz jeszcze nie wiedzieć czym to grozi. To, że łamiesz zasady projektowania obiektowego – mogłeś nie zauważyć. Ale, że masz w dwóch różnych miejscach (CostsController i CarsController) mechanizm robiący to samo i Ci to nie przeszkadza – za to właśnie trafisz do piekła.

Powtórzenie, które przed chwilą opisałem ma dwie poważne konsekwencje:
1. Mechanizmy dbające o spójność bazy danych masz rozproszone w wielu klasach. Dlatego zmiana reguły biznesowej pociąga za sobą konieczność zmiany wielu dublujących się wierszy kodu. Łatwo wtedy o pomyłkę.

2. Model nie dba samodzielnie o spójność danych. Pisząc w ten sposób aplikację zakładasz, że każdy programista w każdym momencie rozwoju aplikacji (czyli nawet za dwa lata) będzie dokładnie pamiętał tą (i wszystkie inne) reguły biznesowe. Dodając kolejny element, znów będzie musiał samodzielnie zadbać o każdy aspekt spójności danych. Łatwo wtedy o pomyłkę.

Dlatego o spójność danych musi dbać warstwa modelu. Bo ona dba o sama siebie. Jeśli odpowiednio zaprojektujesz model Cost, przy pomocy callback’ów beforeValidate, before i afterSave zawrzesz w modelu zapewnienie spójności, wszystko co pozostanie w przyszłości do zrobienia to przygotowanie odpowiedniej tablicy w kontrolerze i wywołanie $this->Cost->save(…).

Dlatego czwartą zasadę formułuję następująco:
mechanizmy dbające o spójność bazy danych umieszczaj w warstwie modelu

ps. Witam wszystkich po przydługiej przerwie wakacyjno-inietylko ;)

Share Button

AppController::beforeFilter() – nagle nie działa?

Jeśli od jakiegoś czasu używasz CakePHP, to wiesz, że istnieje metoda kontrolera beforeFilter(). Jest to tak zwany callback wywoływany tuż po wykonaniu akcji kontrolera, ale przed renderowaniem widoku (źródło).

Dość częstą praktyką jest definiowanie metody w pliku app/app_controller.php, w klasie AppController po to, aby wykonywać w niej operacje, które zawsze muszą być wykonane przed każdą akcją kontrolera. Jednak czasem zdarza się, że jednocześnie inny programista zdefiniuje metodę beforeFilter() w swoim kontrolerze, wtedy niedoświadczony programista może mieć problemy ze znalezieniem przyczyny “dlaczego mój beforeFilter() w AppController się nie odpala?”.
Powód jest prosty, jeśli dobrze rozumiemy zasady OOP (których w tym blogu przybliżał raczej nie będę). Zatem jeśli chcemy, aby nasza AppController::beforeFilter() odpalała się zawsze, niezależnie od tego, czy w jakim kontrolerze jest zdefiniowana beforeFilter() czy też nie, należy w kontrolerach umieszczać wywołanie metody z klasy AppController. Osiągamy to oczywiście za pomocą parent::beforeFilter().

Share Button