Chałupniczy DDD, czyli jak wydzielić logikę biznesową

17
lip/12
4

Są pewne teorie, które zdobywają sporą popularność w świecie programowania, a nie są tak naprawdę niczym nowym. Przykładowo wzorce projektowe to przecież zunifikowany zbiór rozwiązań pewnych problemów. Podobnie jest z DDD, czyli Domain-Driven Design, który zbiera wszystko to co już wszyscy wiemy lub powinniśmy wiedzieć nota bene poprawnego projektowania aplikacji i raczy nas tą wiedzą z zaskakującą świeżością. Z drugiej strony, może te zasady wydają się tak oczywiste po ich poznaniu, że do głowy wpada pomysł iż DDD wydaje się po prostu odtwórcze? Podejście DDD to oczywiście sporo teorii, słownictwa i zasad, ale w gruncie rzeczy sprawa jest relatywnie prosta. Zaraz ją wspólnie ugryziemy.

Nie ma rzeczy idealnych, czasami trzeba zrobić coś na wczoraj albo zdarzają się sytuacje w których pomysł na biznes wydaje się tak dopracowany, tak perfekcyjny, że aż nie wierzymy iż może się zmienić. A jednak. Moment, gdy aplikacja jest wdrażana i wszystkie procesy są doskonale rozumiane jest bardzo komfortowy. Pomysł na biznes zawsze ewoluuje, a co gorsza często zmienia się kompletnie. Dodawane są nowe sposoby sprzedaży, integrowane zewnętrzne usługi, a z czasem pojawiają elementy systemów ERM (fakturowanie, obsługa klienta) przez co zależności pomiędzy elementami w których prym wiodą produkt, klient i sprzedaż stają są niebanalne. Wspominam o tym ponieważ szczególnym wyzwaniem staje się szybkie reagowanie na zmiany, łatwość wprowadzenia w system nowym członków zespołu developerskiego. Nowe osoby, które mając wiedzę tylko na temat podstawowych funkcjonalności nie będą w stanie rozwijać procesów, które aplikacja realizuje. Pierwszy pomysł, czyli szybkie spojrzenie na strukturę bazę danych nie da nam jakiegokolwiek pojęcia co dzieje się w aplikacji, na przykład: jakie procesy zachodzą, gdy kupowany jest produkt. Nie odpowie nam na pytanie na czym polega składanie zamówienia, co może się z nim stać na przestrzeni kolejnych dni, jak system reaguje na zewnętrzne zmiany, opóźnienia czy nadpłaty.

Krok by pokonać przeszkodę nie wygląda skomplikowanie, jest oczywisty: wyodrębnij całą logikę biznesową i nie zaśmiecaj jej, wypracuj wspólny język między programistami, a biznesem tak, by stworzona abstrakcja systemu i procesów była doskonałym odzwierciedleniem rzeczywistości. To esencja DDD. Nie mówimy tu o przeniesieniu zapytań do bazy danych w jedno miejsce, czyli uporządkowania tzw. spaghetti, interfejsu wymieszanego z logiką i dostępem do danych – tu nie o to chodzi. Nasz system odzwierciedla pewien skrawek rzeczywistości, opisywany pewnym zunifikowanym słownictwem. Jeśli chcemy zmienić kółko na kwadrat, obie strony muszą rozumieć czym kółko i kwadrat jest. Nie chodzi nam przecież o kolor zielony, czy zmianę na romb czy trapez. To trywialny przykład, oczywiście, ale już na takim poziomie pewne nieporozumienia się zdarzają – zapytajcie dowolnego designera.
Ten skrawek rzeczywistości, czyli domena naszej aplikacji zmienia się dynamicznie: przychodzą klienci i rejestrują się, zakupują i aktywują pewne usługi, wysyłamy im powiadomienia o fakturach, które są opłacane bądź na wiele różnych sposób, a płatności i usługi dzielone, i łączone zależnie od humoru zespołu marketingowego. Produkty i klienci z czasem zmieniają swój status co jest aktywatorem kolejnych procesów i tak w nieskończoność. Fakty o tym co dzieje się podczas naszych procesów należy co najważniejsze zrozumieć, próbując zminimalizować ryzyku wystąpienia konfliktu między tym co rozumie dział techniczny i ekspert, najczęściej właściciel tego procesu. Najlepiej sprawdza się więc kartka i długopis plus dużo cierpliwości.

Wiele na temat DDD możecie przeczytać w internecie, czy to na slideshare, czy kupując specjalistyczną książkę. Tutaj skupię się na małym studium przypadku, który był mi bliski. Niech więc stanie się DDD (domain-driven design).

Mój przypadek polegał na usprawnieniu architektury już istniejącej aplikacji. Problem wiąże się niedbalstwem i tzw. grubym kontrolerem, czyli logiką biznesową nie tam, gdzie powinna być. Usługa biznesowa i procesy koniec końców rozrastały się, co ostatecznie sprawiło, że nikt dokładnie nie wiedział jak one działały. Fatalna sprawa w praktyce, szczególnie gdy przychodzi do wprowadzania zmian. „Co zmienić? A jak to teraz działa?”. Remedium miały być „punktowe” poprawki i dodatkowo w każym nowym elemencie systemu stosowanie podejścia DDD, a przynajmniej pewne jego podstawowe założenia. Cele:

  • rozdzielenie UI i logiki biznesowej (teraz część logiki wciąż pozostaje zapisana w akcjach i kontrolerach, nie w modelu),
  • rozdzielenie dostępu do danych i logiki biznesowej (nasze modele są jednocześnie abstrakcją dostępu do danych).

Myślę, że te punkty same w sobie niewiele mówią, posłużę się więc przykładem. Gdy użytkownik kupuje produkt jest to pewna interakcja, która nie powinna być zapisana gdziekolwiek poza modelem, to wiemy. Nie chcemy też by zakodowana była jako szybkie zapytanie do bazy danych, która jest tylko naszą przechowalnią, magazynem informacji. Kluczem operacji zakupu nie jest to jak zmieniają się dane w bazie, ale to w jaki sposób zmieniają się stany „encji” – bytów w naszym systemie. Czy użytkownik otrzymuje coś w procesie zakupu? Jak zmienia to jego stan? Czy inne byty są przy tym modyfikowane? Magazyn? Kontakt z działem handlowym? Trudno byłoby czytelnie zaprogramować taki zestaw zależności, gdybyśmy posługiwali się tylko zapytaniami do bazy i paroma pomocniczymi funkcjami. Nasz system powinien prezentować prawdziwie zorientowane-obiektowo podejście. Czemu by nie stworzyć dwóch klas: „użytkownik” i „produkt” by obiekt wybranego użytkownika mógł dosłownie wejść w interakcję z produktem i go kupić? Moglibyśmy wtedy napisać:

class User {
  private $money = 120;
  private $points = 0;
  private $products = array();
  public function buy( Product $product )
  {
     $this->products[] = $product;
     $this->money -= $product->getPrice();
     $this->points += $product->getLoyaltyProgram();
  }
}

$product = new Product("Laptop Asus");
$user = new User("Jacek");
$user->buy($product);

Wygląda lepiej niż „INSERT INTO users_products…” by przyporządkować produkt do użytkownika, prawda? W powyższym kodzie nie ma ani słowa o tym jak i gdzie zapisywane są dane. Klasy nie są abstrakcjami rekordów w bazie danych, tzw. encja jest tylko abstrakcją rzeczywistości związanej z użytkownikiem i produktem. Mówimy „użytkownik kupuje produkt” i tak zapiszemy to w naszym API. Krótkie spojrzenie na użytkownika pozwala nam zrozumieć wiele o procesie zakupu. Widzimy, że użytkownik może mieć wiele produktów, że posiada pewien portfel i zdobywa punkty w programie lojalnościowym.

Każda klasa to proste POPO (Plain Old PHP Object), nie dziedziczy po żadnej klasie typu ActiveRecord, nie jest związana z żadną tabelą w bazie danych:

class User {
  // ...
  public function buy( Product $product ) {}
}

Jest to prosty byt w obrębie naszej aplikacji, przez co reprezentuje tylko logikę biznesową, fragment rzeczywistości na którym operujemy. I nic więcej. Reszta to detale implementacji i tymi powinny pozostać.

Takie podejście w przeciwieństwie do Active Record, które może się w tym miejscu kojarzyć, ma kilka specyficznych zalet. Mianowicie nie jest to ORM, czyli mapowanie w stosunku 1:1 struktury bazy danych. Podejście jest o tyle przydatne, gdy rozdzieliliśmy ogromną tabelę na kilka mniejszych by zwiększyć wydajność (vertical partitioning) i chcemy połączyć tych kilka rekordów i umieścić je w jednym obiekcie. Ma zalety także, gdy połączyliśmy kilka tabel w jedną (denormalizacja) by ograniczyć ilość zapytań z JOIN i chcemy jeden rekord pobrany z bazy rozdzielić na kilka pomniejszych logicznych obiektów. Możliwość mapowania danych zaciąganych z bazy i umieszczania ich w dowolnych klasach staje się bardzo użyteczna. Mówimy tu o koncepcji, tzw. data mappera.

Data mapper miałby konwertować dane pomiędzy naszą bazą daną, a obiektami; jednocześnie sprawiając, że obiekty nie będą wiedziały nic o bazie danych czy jakiejkolwiek innej pamięci w której będą zapisane. Za pomocą podstawowych operacji na obiekach i wartościach zmieniamy stan naszych encji i na koniec zapisujemy je, także z pomocą „data mappera”.

O samej implementacji opowiem już niedługo.

Otagowane jako: , ,
Komentarze (4) Odniesienia (0)
  1. snapshot
    13:04 on Lipiec 18th, 2012

    Super tekst. Właśnie planuję wdrożyć DDD do najnowszego projektu na zendzie, którego start developerki planowany jest na dniach. Muszę przygotować jakieś zalety tego rozwiązania, ale widzę że będzie ciężko ich przekonać. W ostateczności zostanę na ActiveRecord – na pewno będzie łatwiej to wdrożyć, po za tym mam na tym polu doświadczenie. Problem z nauką DDD jest brak konkretnych informacji, jak i rozbieżność. Jak używać i definiować services, jak zapisywać encje w repo (zamieniać na tablicę gettery – co w przypadku dodania nowego pola? Trzeba zmieniać to w wielu miejscach). Wydaje mi się, że dziedziczenie Zend_Db_Table_Row_Abstract + services + value object powinno utrzymać kod w dostatecznej jakości.

  2. Damian Tylczyński
    19:32 on Lipiec 18th, 2012

    Dzięki! Mam wrażenie, że podejmujecie dobrą decyzję trzymając się Table Gateway, który dostarcza Zend Framework z paroma dodatkami. Istotą DDD nie są szczegóły implementacji, a z drugiej strony bez fachowca, który ma doświadczenie w DDD budując nowy system może być kiepsko. Zawsze lepiej trzymać się narzędzi, które się zna niż brnąc w nieznane z dużym projektem. Eksperymentować zawsze możecie z boku projektu. Nigdzie nie znalazłem narzędzi pod projektowanie z podejściem DDD dla PHP, które mógłbym Wam polecić.

  3. snapshot
    21:11 on Lipiec 18th, 2012

    Nie liczyłem na żadne narzędzia. Bardziej chciałbym zobaczyć jakiś nieduży, ale realny projekt w DDD. Zawsze daje to lepszy pogląd na temat niż teoria i małe fragmenciki kodu. Czekam w takim razie na wpis o implementacji :-)

  4. Damian Tylczyński
    21:52 on Lipiec 18th, 2012

    Ja także chciałbym coś takiego zobaczyć :) Przy kawie możesz zagłębić się w kod http://dddsample.sourceforge.net/ Jest to implementacja przykładu z książki „Domain-Driven Design: Tackling Complexity in the Heart of Software”. Język to Java.

Niestety, skomentowanie tego wpisu jest niemożliwe.

No trackbacks yet.

Optimization WordPress Plugins & Solutions by W3 EDGE