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 ;)

Dobre praktyki programowania w CakePHP #3

Tym razem krótka notka…

Zauważyłem, że czasem gdy stosujemy wzorzec MVC, zapominamy* że istnieje coś takiego jak projektowanie i programowanie zorientowane obiektowo.
(*)zakładając, że wcześniej wiedzieliśmy, że coś takiego istnieje – to nie jest takie oczywiste.

Chodzi mi o to, że jak piszesz class, private i extends to jeszcze za mało, żeby powiedzieć iż Twój kod jest obiektowy. Na razie możesz powiedzieć, że masz klasy i metody. Ale dalej możesz mieć bajzel jak bez nich.

Dlatego powstają takie niespodzianki jak metoda getAllComments($postId) w kontrolerze Posts, bo akurat wyświetlając dany post potrzebujesz wyświetlić komentarze. Chwała i tak się należy, bo ktoś umieścił ten fragment w osobnej metodzie. Jednak to jeszcze za mało, żeby nie musieć się kodu wstydzić.

Jeśli przypomnisz sobie podstawy programowania obiektowego ze studiów, czy książki o C++ może taki przykład wyda Ci się znajomy:
- Auto jest obiektem: posiada atrybuty szybkość_maksymalna, ilość_pasażerów oraz metody jedź(), ruszaj(), zatrzymaj_się(), włącz_klimę() ;)
To jest przykład w którym wyjaśnia się czym są obiekty, jak ich używać. Czasem brakuje jednak informacji – prostej odpowiedzi na pytanie:
“Dlaczego klasa Auto nie posiada metody wypij_kawę_w_samochodzie()?”. To już zagadnienie z OODesign.

W przypadku samochodu jest to oczywiste, ale łatwo zapomnieć o tym, kiedy nasze klasy są mniej “dotykalne” i sami je projektujemy, a nie “mapujemy” ze świata rzeczywistego. Dlatego skoro wiemy, że metoda wypij_kawę_w_samochodzie() powinna należeć do klasy Człowiek – to dlaczego każemy klasie Posts znajdować komentarze?

Dochodzę już do sedna: przy projektowaniu aplikacji w CakePHP (i w każdym innym frameworku opartym na MVC lub nie) nie możesz olewać zasad projektowania obiektowego. Nie możesz ignorować problemu przynależenia metod do odpowiednich klas. Dlaczego?

Przykład:
Gdy w kilka osób rozwijacie aplikację i nie mieliście 10tysięcy na wynajęcie architekta, który zaprojektowałby wszystkie klasy i ich metody, to prawdopodobnie rozwijacie architekturę stopniowo. W przypływie twórczości jeden z kolegów umieścił metodę getAllComments() w kontrolerze Posts. Następnie Ty na stronie główniej musisz umieścić ostatnie komentarz. Rzucasz okiem na klasę Comments i widzisz, że brakuje tam metody getAll() (Comments w domyśle) więc ją implementujesz.
W wyniku łamana jest reguła DRY, a co za tym idzie powstaje redundancja w kodzie. Zmniejsza się prostota projektu i rośnie dług techniczny.

Może się wydawać, że to błahy przykład i to jest dobre wrażenie. Jednak jeśli nie myślisz o tym problemie takich kwiatków w projekcie prędzej czy później będziesz miał całą łączkę. Po jakimś czasie łączka zarasta tak bardzo, że nie sposób tam wejść z maczetą, więc zarzucasz projekt, bo nie nadaje się on już do konserwacji.

Zatem zasada numer 3:
Poznaj zasady projektowania obiektowego, i nie wypinaj się na nie podczas programowania

Zarządzanie wersjami STRUKTURY bazy danych w cakePHP 1.2

W poprzednich postach (m.in. zarządzanie wersjami oprogramowania) udało mi się nakreślić problem przy zarządzaniu oprogramowaniem pojawiający się na styku kod-baza danych. Nawet mogę powiedzieć, że mały sukces na tym polu odnotowałem przy pomocy ImageBehavior, jednak jeśli chodzi o strukturę – ciągle zmagałem się do tej pory z przeciwnościami.

Jednak okazuje się, że cake w nowym wydaniu wychodzi nam na przeciw razem z klasą Schema, oraz z narzędziem konsolowym ./cake schema … po krótce opowiem o co chodzi.

Zabawę z tym narzędziem najlepiej zacząć mając już jakiś zalążek aplikacji (tabele + modele). Jeśli sprawiamy ten podstawowy warunek możemy wpisać w konsoli ./cake schema generate … ot tak, dla jaj.

Następnie możemy się w katalogu app/config/sql/ namierzyć plik schema.php. To właśnie artefakt wygenerowany przez nas przed sekundą. Można w celach samorozwojowych zajrzeć do środka…

Jednak ciekawe rzeczy dzieją się, kiedy ponownie wywołamy to samo polecenie: otóż cake rezolutnie zauważy, że plik schema.php już istnieje i zapyta nas co dalej. Polecam wybór opcji [S]napshot i ponowny rzut oka do wspomnianego wyżej katalogu. Co widzimy? Dokładnie! Nowy plik o nazwie schema_2.php :D Zachęcam do zapoznania się z helpem (./cake schema help).

Wystarczy, że teraz przekonam zespół, aby w sytuacji, gdy nastąpiły zmiany w bazie, przed commitem wywołali to polecenie. Jest jeden problem, którego ewentualnie można się spodziewać – sporadycznych konfliktów. To znaczy sytuacji, w której dwóch programistów:

  1. ściąga repozytorium, 
  2. dokonuje (nawet różnych) zmian w bazie, 
  3. zatwierdza dane: 
    1. schema generate, 
    2. svn add schema_X.php, 
    3. svn commit

Problem w tym, że w takiej sytuacji w punkcie 3.3 jeden z nich dostanie informację

Nie mogę dodać schema_X.php do repozytorium, gdyż takowy  już w repozytorium istnieje.
Z poważaniem Twój
SVN

Nie jest to jakaś wielka tragedia, jak przy każdym konflikcie trzeba będzie go rozwiązać (w tym wypadku przy spotkaniu tych dwóch programistów). Jednak myślę, że takie sytuacje można by zlikwidować wywołując tą sekwencję w jednym ciągu (nie np. commit po dwóch godzinach od schema generate), może nawet napisać prosty skrypt, który załatwi to za nas (taki svncommitwithcakeschemagenerate.sh ;))