Prosta implementacja DDD

18
lip/12
2

Jest to kontynuacja poprzedniego wpisu dot. Domain-Driven Design. Chciałbym opisać prostą i eksperymentalną implementację DDD, którą stworzyłem w swoim czasie wolnym. Nie jest to kompletny system do zarządzania logiką biznesową, jest to jednak mały zaczątek, który może być podstawą bądź inspiracją dla innych rozwiązań.

Jak pisałem wcześniej, głównym celem DDD jest zaprogramowanie wolnej od „szumów” logiki biznesowej, zapisanej zrozumiale z jak największym naciskiem na odwzorowanie rzeczywistości. Będziemy więc operować na Plain Old PHP Objects (POPO) nie dziedziczących po żadnych klasach abstrakcyjnych (szablonach) i nie implementujących żadnego interfejsu. Kod powstał w czerwcu, lecz dopiero teraz znalazłem czas aby opisać cały proces jego powstawania dokładniej.

Aby zaimplementować założenia potrzebujemy dwóch elementów:

  • encji – reprezentującą opisywaną biznesowo jednostkę, która reprezentowana jest przez zbiór swoich atrybutów,
  • repozytorium – miejsce zawierające metody pozyskiwania encji.

Przykładowo Active Record, spopularyzowany przez framework Ruby on Rails wzorzec ORM (mapowania struktury bazy danych na obiekty) implementuje tak encje jak i repozytoria w jednej klasie.

class User extends ActiveRecord\Record {
  static public function get($id) {}
  public function getName() {}
  public function save() {}
}

W prezentowanym przypadku rozdzielimy źródło danych od reprezentacji rekord (u nas encji):

namespace Repository {
  class  User {} // tutaj metoda `get($id)` i `save(Entity\User $user)`
}

namespace Entity {
  class User {} // tutaj metoda `getName()`
}

Pozwoli nam to na stworzenie encji z wielorakich źródeł poprzez podmianę repozytorium. Repozytorium będzie w stanie zapisać stan użytkowników jak i odczytać ich – zapisać stan encji i zwrócić encje po identyfikatorze. Odseparowujemy, więc od siebie źródło danych i logikę biznesową. Repozytorium pomimo swojej prostoty ma kilka praktycznych problemów, a może raczej wyzwań, które komplikują implementację:

  • Czas życia encji w obrębie aplikacji. Powinniśmy zwracać singleton czy zawsze nowy obiekt? Nie chcemy by każde odwołanie się do metody `get()` wywoływało zapytanie do bazy danych. Nie chcemy także by w obrębie systemu działało wiele encji-kopii, musimy upewnić się, że jedna kopia, jednego użytkownika będzie przekazywana do wykorzystania we wszystkich procesach.
  • Optymalizacja szybkości pozyskiwania obiektów „User”. Nie chcemy najpierw wysyłać do bazy skomplikowanego zapytania z mnóstwem JOIN, a następnie używając identyfikatora (klucza główny w bazie danych) wywoływać `get($id)` dla każdego rekordu zwróconego przez skomplikowane zapytanie. Dałoby nam to N+1 zapytań do bazy. Wolelibyśmy poprzestać na jednym zapytaniu i zbudować encje używając już zwróconych danych.
  • I ostatnie wyzwanie to już oczywiście problem zaprogramowania mapy atrybutów encji „User” na pola w bazie danych. Przecież imię i nazwisko zapisane w bazie może byćreprezentowane w encji jako pojedynczy atrybut „name”.

Zacznijmy od początku. Mając proste obiekty-encji w jaki sposób możemy uzyskać dostęp do ich atrybutów?

class User
{
  protected $name;
}

Oczywiście jesteśmy w stanie dopisać tzw. gettery, czyli w tym wypadku metodę `getName()` by uzyskać wartość chronionego atrybutu. Jednakże nie wszystkie atrybuty encji powinny być publiczne. Z drugiej strony metoda pozyskiwania imienia użytkownika może nosić w sobie znamiona metody, która pewną reprezentację wartości – imienia, nie jego oryginalną formę. Może na przykład upewnić się, że jest pisane z dużej litery. Co możemy zrobić w tym wypadku? W PHP nie mamy do dyspozycji przyjaźni pomiędzy klasami. Nie możemy zdefiniować, że repozytorium jest „przyjacielem” naszej klasy użytkownika i ma dostęp do jego pol nie-publicznych. Z pomocą przychodzi mechanizm reflekcji.

$entity = new \Entity\User;
$class = new ReflectionClass(get_class($entity));
foreach( $class->getProperties() as $prop ) {
  // umożliwamy sobie, w czasie działania refleksji,
  // dostęp do pól nie-publicznych
  $prop->setAccessible(true);
  echo $prop->getValue($entity);
}

Z pomocą tej samej klasy „ReflectionClass” jesteśmy w stanie także ustawiać wartości w polach obiektu. Mamy więc do dyspozycji mechanizm „boga”, który może modyfikować wartości innych obiektów. Nie potrzebujemy do tego dodatkowych, co gorsza publicznych, metod w klasie encji typu `setName($name)`, które burzyłyby zasadę enkapsulacji, jedną z podstawowych zasad programowania zorientowanego obiektowo. Encja pozostaje więc wciąż prostą klasą PHP.

By uniknąć duplikowania obiektów o tej samej tożsamości w obrębie aplikacji musimy owe encje przechowywać w pamięci i przy każdym kolejnym żądaniu zwracać encję z pamięci, nie tworzyć jej kolejnej kopii na podstawie danych z bazy. Baza danych jest dla nas tylko formą „uwieczniania” stanu encji, po prostu jej zapisu. Do zaimplementowania tego mechanizmu potrzebujmy po prostu sposobu na uzyskanie identyfikatora encji, nawet sztucznego klucza naturalnego znanego z MySQL pod postacią „auto increment”. Dla użytkownika może być to adres e-mail, który będzie indeksem głównym i właśnie z wykorzystaniem tej wartości będziemy później przechowywać użytkowników w pamięci aplikacji. Jak zaimplementować w encji formę identyfikatora tak by nie wymuszać w niej sztucznych implementacji typu metod `getId()`? Tutaj także przychodzi z pomocą „ReflectionClass”, który potrafi nie tylko odczytywać pola i metody klasy, ale także komentarze przy nich.

class User
{
  /**
  * @id
  */
  private $email;
  protected $name;
}

Z pomocą takiej prostej konwencji, umieszczania w komentarzu tagu „@id” możemy zidentyfikować i pobrać wartości pól, które składają się na identyfikator encji.

foreach( $class->getProperties() as $prop ) {
  if( strpos($prop->getDocComment(), '@id') ) {
    echo $prop->getName() . ' jest identyfikatorem';
  }
}

Pozostaje jedynie zebranie wartości wszystkich pól identyfikatorów i możemy zbudować złożony identyfikator, który może być na przykład kluczem tablicy pod którym będziemy przechowywać już pobrane wcześniej encje.

W najbliższym wpisie opiszę jak te wszystkie elementy złożone razem mogę posłużyć za prostą podstawę zarządzania encjami. Opiszę także proste mapowanie danych z bazy za pomocą prefiksów, które pozwolą nam rozpoznać, które pole w bazie powinno być jakim atrybutem encji. Do zobaczenia niebawem.

Otagowane jako: , ,
Komentarze (2) Odniesienia (0)
  1. snapshot
    12:06 on Lipiec 23rd, 2012

    Czekam na więcej, ale widać że bez Reflection ani rusz. W sumie to standard wśród ORMów (np. Doctrine2)

  2. Alex
    17:01 on Listopad 29th, 2013

    Wybacz, ale uwazam ze uzywanie Refleksji do dostawania sie do pol prywatnych to po prostu zla rzecz.

    Zastanowmy sie – Po co deklarujemy pola/metody jako prywatne? Ano po to, aby nikt inny nie mogl ich tknac. Niemniej jednak, to wcale nie oznacza, ze blokujemy sobie calkowicie dostep do modyfikacji. Pola dla takiego modelu musza byc zainicjalizowane, a co za tym idzie – uzywamy do tego konstruktora. Czysto, legalnie. Po stworzeniu obiektu pola sa juz nietykalne, czyli dokladnie tak jak bysmy tego chcieli.

    No, to tyle ;) imo brakuje nieco artykulow nt. DDD w internecie wiec moze sam sie pokusze cos napisac.

    Pozdro ;)

Niestety, skomentowanie tego wpisu jest niemożliwe.

No trackbacks yet.

Optimization WordPress Plugins & Solutions by W3 EDGE