Blog
Walcz z anemią

Spotkałem się ze stwierdzeniem, że anemiczny model to taki, który jest złożony z obiektów zawierających settery i gettery do wszystkich swoich właściwości. Według mnie jest to anemiczny opis anemicznego modelu :).
Czym jest anemia modelu domenowego?
Anemia oznacza niedobór, ale niedobór czego? Przyjrzyjmy się bliżej próbce kodu.
// create new invoice draft $invoice = new Invoice(); $invoice->setStatus(new InvoiceStatus(InvoiceStatus::DRAFT)); $invoice->setCustomer($customer); $invoice->setSeller($seller); $invoice->setPaymentType($paymentType); $invoice->setItems($items); // issue an invoice $invoice->setStatus(new InvoiceStatus(InvoiceStatus::ISSUED)); $invoice->setNumber($number); $invoice->setIssueDate($issueDate);
Widać, że faktura to obiekt złożony z wielu parametrów, widać również, że można ten obiekt wprowadzić w różne stany poprzez modyfikację wybranych parametrów. Ile jest takich stanów? Przy założeniu, że każde pole może mieć konkretną wartość badź NULL to stanów mamy 2^[liczba parametrów]. W naszym przypadku 2^7 = 128. W ilu poprawnych stanach może znajdować się faktura z punktu widzenia domeny? – Zapewne kilku. Nasza aplikacja musi zatem wychwycić wszystkie błędne stany i rzucić wyjątek. Ponieważ obiekt faktury nie realizuje walidacji poprawności swojego stanu, to odpowiedzialność ta spoczywa na kodzie klienta operującego na fakturze.
Pierwszy objaw anemii – model nie odpowiada za spójność swoich danych. Kod klienta może wprowadzić go w niewłasciwy stan.
W naszym przykładzie widać, że faktura może być w dwóch stanach – wersja robocza (draft), wystawiona (issued). Co więcej, z punktu widzenia domeny, faktura zawsze ma dane sprzedawcy, nabywcy i formę płatności. Aktualny model faktury nie eksponuje takiego wymagania. Idąc dalej widać, że aby wystawić fakturę należy zmienić jej status, ustawić numer i datę wystawienia. Co, jeśli te trzy linijki w kodzie operującym, nie byłyby opatrzone komentarzem? Czy byłoby jasne, czemu zmieniane są akurat te trzy pola?
Drugi objaw anemii – model nie jest wyrazisty, nie widać intencji w jego API. Kod klienta musi wzbogacić operacje na modelu o wyrażenie intencji – poprzez komentarze lub otoczenie metodą o wyrazistej nazwie.
Walka z anemią
Spróbujmy rozwiązać oba te problemy. Załóżmy, że istnieje wymaganie mówiące, że faktura musi mieć sprzedawcę, nabywcę, sposób płatności i przynajmniej jedną pozycję. Skoro tak, to wystarczy wymagać w konstruktorze, aby zawsze podane były te parametry. Załóżmy też, że faktura nigdy nie może być utworzona od razu jako wystawiona, a jedynie w formie wersji roboczej.
class Invoice { function __construct(Customer $customer, Seller $seller, PaymentType $paymentType, InvoiceItemList $items) { if ($items->isEmpty()) { throw new InvalidArgumentException('At least one invoice item is required'); } $this->status = new InvoiceStatus(InvoiceStatus::DRAFT); $this->customer = $customer; $this->seller = $seller; $this->paymentType = $paymentType; $this->items = $items; } }
Pozostaje kwestia zamodelowania operacji wystawienia faktury. Nie chcemy wymagać od klienta, aby wiedział jakie parametry faktury powinien zmodyfikować, aby wprowadzić ją w poprawny stan wystawienia. Dodatkowo, model powinien pilnować, aby operacja ponownego wystawienia faktury już wystawionej zakończyła się błędem.
class Invoice { function issue(InvoiceNumber $number, Date $issueDate) { if ($this->status == InvoiceStatus::ISSUED) { throw new Exception('Invoice has been already issued'); } $this->status = new InvoiceStatus(InvoiceStatus::ISSUED); $this->number = $number; $this->issueDate = $issueDate; } }
Po tych zmianach kod operujący na modelu faktury wygląda tak:
$invoice = new Invoice($customer, $seller, $paymentType, $items); $invoice->issue($number, $issueDate);
Problem dużej liczby parametrów konstruktora
W pierwotnej wersji model tworzony był etapami poprzez wywołania odpowiednich setterów. Kolejność ich wywoływania mogła być dowolna. W aktualnej wersji konstruktor wymaga podania 4. parametrów i łatwo można sobie wyobrazić, że mogłoby być ich jeszcze więcej.
Wywołanie wieloargumentowego konstruktora wymaga większego wysiłku od programisty i jest niewygodne, gdy w aplikacji jest kilka miejsc, gdzie konstruujemy obiekt faktury. Czy jest to kompromis, na który musimy się zgodzić rezygnując z anemicznego modelu? Nie. Istnieje wzorzec konstrukcyjny, który przychodzi z pomocą – Budowniczy (Builder)
class InvoiceBuilder { private $customer; private $seller; private $paymentType; private $items; public function setCustomer(Customer $customer) { $this->customer = $customer; return $this; } public function setSeller(Seller $seller) { $this->seller = $seller; return $this; } public function setPaymentType(PaymentType $paymentType) { $this->paymentType = $paymentType; return $this; } public function setItems(InvoiceItemList $items) { $this->items = $items; return $this; } public function build() { return new Invoice($this->customer, $this->seller, $this->paymentType, $this->items); } }
Kod operujący na modelu:
$invoice = (new InvoiceBuilder()) ->setCustomer($customer) ->setSeller($seller) ->setPaymentType($paymentType) ->setItems($items) ->build(); $invoice->issue($number, $issueDate);
Geneza
Na zakończenie wymienię kilka przyczyn powstawania anemicznych modeli:
Ograniczenia bibliotek ORM. Spotkałem się z bibliotekami typu ORM, które zakładały, że encje muszą mieć bezargumentowy konstruktor, co jest równoznaczne z ustawianiem wszystkich pól poprzez settery.
Ewolucja aplikacji CRUD. Istnieją aplikacje, które są, de facto, prostą nakładką na bazę danych, pozbawioną jakiejkolwiek logiki. Z czasem, gdy pojawiają się coraz to bardziej złożone wymagania, rozwija się logika biznesowa, ale nie w ramach modelu, a w kodzie na nim operującym.
Przeciek obiektów transportowych DTO. Warstwa prezentacji danych (GUI, WebService) może wymagać danych w postaci obiektów transportowych, które z definicji pozwalają na swobodną manipulację swoim stanem. Są to struktury danych. Może się zdażyć, że obiekty te “wyciekną” w głąb aplikacji i będa włączone do modelu domenowego.
Kopiowanie code snippets. Każda biblioteka udostępnia przykłady użycia. Wystarczy otworzyć dokumentację popularnych ORM – Doctrine, Propel, a ujrzymy takie przykłady tworzenia encji:
Trzeba pamiętać, że code snippets najczęsciej mają na celu pokazać naprostsze przykłady użycia, a nie najlepsze praktyki.
The end
A czy Twoje aplikacje mają bogaty model domenowy? Może masz doświadczenie z wychodzeniem z anemii modelu? Czekam na komentarze.