Fat model, skinny controller – przykład

Niedawno przy projekcie trafiliśmy na ciekawy problem. Chciałbym się podzielić z Wami tym czego się nauczyliśmy.

Problem przedstawię w bardzo uproszczonej formie, bo trudno bez machania rękoma przy zabazgranej tablicy wyjaśnić go w całości.

Problem

Mamy formularz dodawania kosztów, który jest dość specyficzny. Można dodać “łysy koszt” i wtedy pojawia się formularz z wyborem firmy i innymi atrybutami. Można jednak z widoku konkretnej firmy wybrać opcję “dodaj koszt” – w takim wypadku lista wyboru firm się nie pojawia.

Dość naturalne jest załatwić to kontrolerem. Są różne możliwości, jedną z nich jest takie zdefiniowanie akcji:

function add($entryId=null) {
   if(!empty($this->data)){
       $this->Cost->save($this->data);
       // redirect w jakieś przyjemne okolice
   }
   if(is_null($entryId)){
      $this->set("entry", $this->Cost->Entry->read(null, $entryId);
   }else{
      $this->set("entries", $this->Cost->Entry->find("list");
   }
}

W widoku wiadomo – jeśli jest $entry, wyświetlimy $entry["Entry"]["name"] i w jakimś ukrytym inpucie wrzucimy id, jeśli istnieje $entries – wyświetlimy selecta.

Na razie w kontrolerze wygląda to wystarczająco zgrabnie, żeby tego nie ruszać. Ale jeśli znajdziesz się w sytuacji, gdy ilość przypadków jest coraz większa – możesz się zorientować, że dodajesz kolejne argumenty do metody add i rozbudowujesz tą metodę. W takiej sytuacji może Ci pomóc takie podejście.

Propozycja rozwiązania

Po pierwsze olej zwykłe parametry funkcji, a zacznij używać parametrów “named”. Czyli zamiast:
costs/add/1/3/45/23
costs/add/2///22
będziesz miał
costs/add/cost_id:1/entry_id:3/param3:45/param4:23
costs/add/cost_id:2/param4:22

Po drugie – przenieś logikę działania do modelu. np:

//model Cost
function giveMeProperData($params){
   if(isset($params["cost_id")){
      // do sth
      return $some_array;
   }
   if(isset($params["entry_id")){
      // do sth
      return $other_array;
   }
   return array();
}

Kontroler uprość do granic możliwości:

function add($entryId=null) {
   if(!empty($this->data)){
       $this->Cost->save($this->data);
       // redirect w jakieś przyjemne okolice
   }
   $this->data = $this->Cost->giveMeProperData($this->params["named"];
}

A w widoku w zależności od tego w jaki sposób wygląda $this->data renderuj widok.
Jeśli w tablicy jest “entry_id” to wyświetl zawartość $this->data["Entry"]["name"], a wartość $this->data["Entry"]["id"] przypisz do jakiegoś ukrytego pola. Itd.

Co zyskujesz?

  1. Kontroler robi to co do niego należy. Przestaje go interesować przypadek właśnie rozpatrywany. Jego interesuje tylko czy został przesłany formularz. Jeśli tak – spróbuj zapisać i przekierować, jeśli nie – pobierz dla niego dane i wyświetl (przekaż do widoku)
  2. Testowanie modeli, choć nie tak proste, jest o niebo łatwiejsze od testowania kontrolerów i widoków – możesz pokryć ważną część aplikacji testami. Możesz testować, czy metoda giveMeProperData() zwraca to, czego się spodziewasz dla konkretnych argumentów.
  3. Jeśli pokryjesz tą metodę testami będziesz mógł w komfortowych warunkach refaktoryzować ten kod – jak się pojawią powtórzenia wyodrębnisz wspólne części itd. Gdyby to siedziała w kontrolerze – nikt nie miałby ochoty tam zajrzeć, a testowanie polegało by głównie na odświeżaniu widoków w przeglądarce dla każdego z przypadków.
  4. Teraz widok jest klasą, która “ma wiedzę” na temat tego jak się zachować w zależności od otrzymanych danych (w prostych przypadkach to już się dzieje – chociażby w akcjach “edit”). Wcześniej część tej wiedzy znajdowała się w kontrolerze.
  5. Przyjemny side-effect wynikający z używania parametrów “named”: Teraz komponując link w widoku zamiast zastanawiać się czy formularz dodawania kosztów dla danej firmy jest pod /costs/add//[id_firmy]/ czy pod /costs/add///[id_firmy] – wiem, że jest pod /costs/add/entry_id:[id_firmy]

Jest oczywiście brak w tym rozwiązaniu – dość sporo kodu wędruje do widoku (który, jak wspomniałem, trudno testować). Jednak pozostawienie architektury w pierwotnym stanie tej kwestii nie rozwiązuje. Na początku masz problem z

  • dużą ilością kodu w kontrolerze
  • kodem kontrolera, który nie jest pokryty testami więc nie będzie refaktoryzowany
  • kodem w widoku, który nie jest pokryty testami

Po zastosowanej zmianie zostanie Ci tylko ostatni punkt. Samodzielnie możesz zdecydować, czy to się opłaca.

Niespodzianka

Zaliczyłem dłuższą nieobecność na blogu przerywaną jedynie odsyłaniem do spamu niektórych komentarzy. Jednak przyszedł czas na zrealizowanie jednego z pomysłów, który chodził mi po głowie.

Chciałbym się podzielić dobrymi książkami, które przeczytałem i które polecam.

Na pierwszy ogień – Coś lekkiego. Head First: Software Development. Świetna pozycja odświeżająca to ciężkie podejście do wytwarzania oprogramowania, które każdy w jakimś stopniu wynosi z czasów akademickich:

Head First: Software Development

Przykłady kodu są wprawdzie w Javie, ale i tak wielki pożytek może przynieść nawet jeśli o Javie wiesz, że ma klasy i składnię podobną do C ;)

Sama książka jest nieco sfatygowana, lekko zaokrąglone rogi od wożenia w plecaku i czytaniu w autobusie i jest tam trochę bazgrołów wykonanych ołówkiem. Ale potraktujcie to jako dodatkową zaletę – nieciekawych książek nie wożę ze sobą, a notuję tylko w tych, które naprawdę pobudzają do myślenia.

Na koniec zasady – jeśli chcesz wygrać tę książkę – dodaj proszę komentarz do tego postu (koniecznie podaj prawdziwy mail). Za tydzień (26 maja) wylosuję zwycięzcę, z którym się skontaktuję za pomocą tegoż maila.

Powodzenia!

Jak zredukować swoją wydajność?

Pewnie jako programiście tak zwane nocki nie są Ci obce. Jeśli studiowałeś – tym bardziej. Jednak zakuwając do egzaminu późno w noc nie zauważysz jednej ciekawej rzeczy – gdy jesteś zmęczony Twoja skuteczność leci na łeb na szyję!

Najłatwiej jest to zauważyć, gdy programujesz i pojawia się problem. Zazwyczaj wtedy musisz odnaleźć źródło problemu, a następnie je usunąć. Ten pierwszy krok jest najtrudniejszy. Oznacza rozpatrzenie kolejnych hipotez co może być powodem, że coś nie działa. W tym momencie musisz brać pod uwagę wiele ewentualności jednocześnie… tu coś zmienić w kodzie, tam coś wywalić do loga, sprawdzić czy coś się zmieniło – wszyscy to znacie.

Work in progress

Teraz przypomnij sobie jak wygląda taka sesja debugowania późną nocą, gdy “już prawie kończysz projekt”. Czy zdarza Ci się zwyczajnie wkurzyć i odejść od komputera? Czy zdarza Ci się w trakcie debugowania zapomnieć jaki efekt miały przynieść te zmiany? Albo gorzej – małe rozproszenie powoduje, że zapominasz co miałeś zrobić? Poszedłeś zaparzyć sobie herbatę i po powrocie nie wiesz od czego zacząć?

Dobrze jest w takich sytuacjach obserwować reakcję swojego ciała i umysłu. Problemy, które o tej porze rozwiązujesz i zajmują Ci godzinę prawdopodobnie “na świeżo” zajmą mniejszą ilość czasu (zaskoczy Cię to, że problem, którego nie rozwiązałeś i poszedłeś spać rozwiązałeś od ręki następnego dnia – znajdując głupią pomyłkę).

Oczywiście czasem nie można się położyć. Ale warto wiedzieć, że takie zjawiska mają miejsce – wtedy można z nimi pracować (lub bardziej modnie – zarządzać nimi). Tak jak łatwo zapominamy o swoim ciele dopóki nie szwankuje, tak łatwo zapominamy, że nasze zdolności umysłowe zmieniają się w ciągu dnia w zależności od zmęczenia, pory, nastroju etc.

Problem programistyczny może być takim papierkiem lakmusowym, który uświadomi Ci jak bardzo jesteś do bani o 3 nad ranem. Jeśli jeszcze studiujesz – może zdecydujesz się położyć, bo zakuwanie w takim stanie jest kompletną stratą czasu.

Życzę Wam miłego i – mam nadzieję – wolnego weekendu :)