Anemic Domain Model - okładka

Anemic Domain Model

Opublikowano Kategorie DDDCzas czytania 11min

Anemic Domain Model to dość podstępny antywzorzec. Bywa problematyczny do wyłapania na pierwszy rzut oka, a gdy już nam się to uda, jego wycofanie również może nie być trywialnym zadaniem. Co więcej, Anemic Domain Model nie zawsze jest antywzorcem. Czasami jego użycie jest racjonalne i w pełni uzasadnione. W tym artykule przyjrzymy się, czym jest Anemic Domain Model, kiedy faktycznie jest antywzorcem oraz w jakich sytuacjach jego użycie może być uzasadnione. Podpowiem też, na co zwrócić uwagę w refaktoryzacji Anemic Domain Model do Rich Domain Model.

Czym jest Domain Model?

Aby zrozumieć, czym jest Anemic Domain Model, warto najpierw wyjaśnić pojęcie Domain Modelu. Domain Model to model domeny biznesowej wyrażony w kodzie, obejmujący Entities, Value Objecty, Agregaty, Domain Services oraz reguły biznesowe opisujące zachowanie systemu.

Zastosowanie Domain Modelu ma sens w przypadku złożonej domeny wykraczającej poza zwykłe CRUD-y. Taka domena najczęściej jest opisana mnóstwem złożonych reguł biznesowych i niezmienników (inwariantów). Niezmienniki to reguły, które zawsze muszą być prawdziwe, np. co najmniej jeden produkt w koszyku, co najmniej jedna subskrypcja przypisana do klienta itp. Naruszenie inwariantu oznacza, że obiekt znajduje się w stanie niepoprawnym z punktu widzenia domeny.

Modele domenowe, w których kluczowa logika biznesowa i inwarianty są osadzone w obiektach domeny, a nie rozproszone w Service Layer, określane są jako Rich Domain Models. Innymi słowy – bogaty model domenowy eksponuje nie tylko stan obiektu, ale również jego zachowanie.

Eric Evans w Domain-Driven Design definiuje Service (lub Application) Layer następująco:

Definiuje zamierzone zadania programu i steruje obiektami dziedziny w celu rozwiązania postawionych przed nim zadań. Zadania, za które jest odpowiedzialna ta warstwa, są znaczące dla biznesu i wymagane w celu poprawnego współdziałania z warstwami aplikacji innych systemów. Warstwa jest utrzymywana w zwięzłej postaci. Nie zawiera reguł ani wiedzy biznesowej, a jedynie zarządza zadaniami i deleguje je do rozwiązania przez obiekty dziedzinowe w kolejnej warstwie niżej. Warstwa nie musi odzwierciedlać konkretnej sytuacji biznesowej, lecz może posiadać stan, ukazujący postęp zadania użytkownikowi lub programowi.

Natomiast Domain (Model) Layer według Evansa to:

Warstwa odpowiedzialna za reprezentację zagadnień biznesowych, informacji o sytuacji biznesowej oraz reguł biznesowych. W tym miejscu jest przechowywany oraz używany stan odzwierciedlający sytuację biznesową, chociaż szczegóły techniczne związane z przechowywaniem tego stanu są oddelegowane do warstwy infrastruktury. Ta warstwa stanowi istotę programu od strony biznesowej.

Czym jest Anemic Domain Model?

Anemic Domain Model to model, w którym obiekty domenowe przechowują dane, ale reguły biznesowe i inwarianty są implementowane poza nimi. Taki model wykorzystuje język domenowy i reprezentuje obiekty domenowe, ale nie zawiera jego zachowań ani reguł biznesowych.

W przypadku Anemic Domain Model problemem jest kod logiki powiązany z modelem rozsiany po klasach wykorzystujących go. Zwykle są to klasy Service Layer, np. klasy typu *Service, *Controller czy *Handler. Klasy te, realizując swoją logikę, operują na modelu, przetwarzają go i wykorzystują głównie jako pojemnik na dane. Gorszym wariantem jest sytuacja, gdy taka logika trafia do współdzielonych utilsów lub helperów. Powoduje to rozproszenie reguł biznesowych i utrudnia zrozumienie domeny.

Warto przy tym pamiętać, że nie każda logika domenowa musi znajdować się bezpośrednio w Entities. W DDD istnieją również Domain Services, które reprezentują operacje domenowe niepasujące naturalnie do pojedynczego obiektu.


class Order {
  public readonly id: string;
  public readonly items: { productId: string; quantity: number; price: number }[];
  public totalPrice: number;
  public status: 'draft' | 'submitted' | 'cancelled';

  constructor( id: string ) {
    this.id = id;
    this.items = [];
    this.totalPrice = 0;
    this.status = "draft";
  }
}

Przedstawiony przykład nie zawiera praktycznie żadnej logiki. Obiekt klasy Order jest jedynie opakowaniem na dane. Brakuje tu na przykład logiki walidującej status zamówienia – czy puste zamówienie może mieć status submitted, czy można anulować zatwierdzone zamówienie. Co więcej, właściwość totalPrice może być zbędna, ponieważ wartość zamówienia da się obliczyć na podstawie pozycji. To jednak również powinno odbywać się w obrębie klasy Order. Poniżej znajdziesz implementację powiązanej klasy OrderService.


class OrderService {
    public addItem( order: Order, productId: string, quantity: number, price: number ) {
        order.items.push( { productId, quantity, price } );
        order.totalPrice += quantity * price;
    }

    public submit( order: Order ) {
        if ( order.items.length === 0 ) {
            throw new Error( 'Cannot submit empty order' );
        }

        order.status = 'submitted';
    }

    public cancel( order: Order ) {
        if ( order.status === 'submitted' ) {
            throw new Error( 'Submitted order cannot be cancelled' );
        }

        order.status = 'cancelled';
    }
}

Przedstawiony przykład jest bardzo jaskrawym reprezentantem Anemic Domain Model. Nawet jeśli klasa Order wystawiałaby gettery i settery, nadal byłoby to widoczne. Już na pierwszy rzut oka można stwierdzić, że większość logiki OrderService powinna zostać przeniesiona do modelu Order. Poniżej znajdziesz zrefaktoryzowany przykład. Warto zwrócić uwagę, że po refaktoryzacji OrderService zajmuje się jedynie wywoływaniem zachowań na modelu.


type OrderStatus = 'draft' | 'submitted' | 'cancelled';

type OrderItem = {
    productId: string;
    quantity: number;
    price: number;
};

class Order {
    public readonly id: string;
    private items: OrderItem[] = [];
    private status: OrderStatus = 'draft';

    constructor( id: string ) {
        this.id = id;
    }

    public addItem( productId: string, quantity: number, price: number ): void {
        if ( this.status !== 'draft' ) {
            throw new Error( 'Cannot modify a submitted order' );
        }

        this.items.push( { productId, quantity, price } );
  }

    public submit(): void {
        if (this.items.length === 0) {
            throw new Error( 'Cannot submit empty order' );
        }

        this.status = 'submitted';
    }

    public cancel(): void {
        if ( this.status === 'submitted' ) {
            throw new Error( 'Submitted order cannot be cancelled' );
        }

        this.status = 'cancelled';
    }

    public getTotalPrice(): number {
        return this.items.reduce( ( sum, item ) => sum + item.quantity * item.price, 0 );
    }
}

class OrderService {
    public addItem( order: Order, productId: string, quantity: number, price: number ) {
        order.addItem( productId, quantity, price );
    }

    public submit(order: Order) {
        order.submit();
    }

    public cancel(order: Order) {
        order.cancel();
    }
}

W rzeczywistych systemach anemiczność modelu nie zawsze jest tak oczywista. Publiczny interfejs klasy może wyglądać poprawnie, a mimo to reguły biznesowe są rozproszone w serwisach, handlerach lub innych częściach aplikacji. Analizując wyłącznie publiczny interfejs modelu, trudno czasami stwierdzić, czy jest on anemiczny.

Problemy wynikające z wykorzystania Anemic Domain Model

Skoro Anemic Domain Model zwykle przedstawiany jest jako antywzorzec, oznacza to, że można zaobserwować negatywne konsekwencje wykorzystania tego podejścia.

Przede wszystkim tracimy korzyści wynikające z programowania obiektowego. Wyciek logiki poza model to utrata enkapsulacji. Dodanie getterów i setterów jej tu nie zapewnia. Zmiana stanu obiektu odbywa się wtedy poza nim, co sprawia, że nie jesteśmy w stanie zagwarantować niezmienników domeny i walidacji reguł biznesowych. Gdy logika znajduje się poza modelem, za każdym razem musimy pamiętać o ręcznym egzekwowaniu reguł i inwariantów. Czasami nie jest to problem – gdyby okazało się, że w systemie idzie stworzyć puste zamówienie, to pewnie świat się nie zawali. Gorzej, jeśli system bankowy pozwoli na wykonanie przelewu mimo braku środków na koncie lub system rezerwacyjny sprzeda to samo miejsce w kinie kilkunastu osobom.

Anemic Domain Model sprzyja również duplikacji kodu. Brak jednego miejsca z logiką sprawia, że rozlewa się ona po systemie, a jej fragmenty są powielane. Każde dodatkowe miejsce, w którym realizowana jest dana logika, to potencjalne ryzyko pomyłki, dodatkowe miejsca do przetestowania oraz ryzyko, że inwarianty nie zostaną zachowane. Może to wynikać z ich pominięcia lub odmiennej implementacji. W przedstawionym przykładzie załóżmy, że chcemy zapisywać datę ostatniej zmiany stanu. Ustawiając wartość pola status, musimy pamiętać o dodatkowym wymogu zapisania daty modyfikacji za każdym razem – zarówno w istniejących miejscach, jak i w tych, które dopiero zostaną dodane.

Trzymanie logiki domenowej w warstwie aplikacyjnej często sprowadza kod do proceduralnej listy kroków. W efekcie system zaczyna przypominać Transaction Script, który sam w sobie nie jest zły. Transaction Script dla prostej domeny jest odpowiednim rozwiązaniem. Jednak jeśli budujemy złożony system, gdzie Domain Model daje wyraźne korzyści, to jego przekształcenie w Transaction Script można już uznać za złe.

Czy Anemic Domain Model zawsze jest zły?

Zgodnie z prawem nagłówków Betteridge’a odpowiedź brzmi: nie. Wykorzystanie Anemic Domain Model bywa świadomą decyzją architektoniczną. Niekiedy wydzielenie fragmentów logiki do Service Layer jest prostsze.

W przykładzie z tego artykułu refaktoryzacja do Domain Model sprowadziła się do przeniesienia kilku if-ów z jednego miejsca w drugie i przeorganizowania publicznego interfejsu klasy Order.

Jednak gdy sytuacja nie jest taka oczywista, pójście na kompromis i wykorzystanie Anemic Domain Model może być wystarczające. Rich Domain Model również ma swoje koszty. Jest to większa złożoność kodu, nadmiarowe abstrakcje, kosztowna refaktoryzacja czy bardziej skomplikowane testowanie.

Temat zasadności wykorzystania Anemic Domain Model celnie podsumował Vlad Khononov w Learning Domain-Driven Design: Aligning Software Architecture and Business Strategy:

The active record pattern is also known as an anemic domain model antipattern; in other words, an improperly designed domain model. I prefer to restrain from the negative connotation of the words anemic and antipattern. This pattern is a tool. Like any tool, it can solve problems, but it can potentially introduce more harm than good when applied in the wrong context. There is nothing wrong with using active records when the business logic is simple. Furthermore, using a more elaborate pattern when implementing simple business logic will also result in harm by introducing accidental complexity.

W przypadku Anemic Domain Model granica między racjonalnym wyborem a antywzorcem jest dość płynna i mocno zależy od kontekstu, w którym go stosujemy.

Jak refaktoryzować Anemic Domain Model do Rich Domain Model

Przed rozpoczęciem refaktoryzacji warto zastanowić się, czy jest ona w ogóle potrzebna. Idąc z duchem pragmatyzmu, być może refaktoryzacja jest zbędna. Jeśli nie przyniesie ona wymiernych korzyści i ma być „sztuką dla sztuki”, to być może nie warto robić jej wcale.

Jeśli jednak dojdziesz do wniosku, że refaktoryzacja jest konieczna, to zachęcam, by rozpocząć ją od identyfikacji Bounded Contextów. Wrzucenie całej logiki do jednego modelu nie jest dobrym pomysłem. Byłoby to przejście z jednej skrajności w drugą. Nie chcielibyśmy skończyć jednej refaktoryzacji i zaraz zaczynać kolejnej. W przykładzie opisanym wcześniej, w części systemu odpowiedzialnej za składanie zamówienia, Order jest czymś innym niż Order w dziale logistyki. Jednych interesuje, czy wartość koszyka się zgadza, a innych, jaki rozmiar paczki wybrać.

Zadbajmy o identyfikację istniejących reguł biznesowych oraz inwariantów w kodzie. Upewnijmy się, że w istniejącym kodzie są jasno zaadresowane na poziomie testów jednostkowych i integracyjnych. Testy te będą konieczne do potwierdzenia, że refaktoryzacja przebiegła pomyślnie i nie wprowadziliśmy regresji. Jeśli takich testów brakuje, to warto rozważyć ich dopisanie przed refaktoryzacją. To jest również moment na wykrycie i ewentualne uspójnienie reguł biznesowych.

W trakcie refaktoryzacji należy również zadbać o odpowiednią enkapsulację modelu, by uniknąć ponownego wyciekania logiki warstwy domeny do warstwy serwisu. Sama refaktoryzacja w miarę możliwości powinna przebiegać stopniowo. Stawiając małe kroki, prościej jest wychwytywać ewentualne błędy i niespójności. Takie podejście jest również wygodniejsze dla osób sprawdzających nasz kod. Warto również wykorzystać metody opisane przez Fowlera i Becka w Refaktoryzacja: Ulepszanie struktury istniejącego kodu:

  • Enkapsulacja Zmiennej (s. 134);
  • Enkapsulacja Kolekcji (s. 170);
  • Zastąpienie Typu Prostego Obiektem (s. 174);
  • Przeniesienie Funkcji (s. 198);
  • Przeniesienie Instrukcji do Funkcji (s. 211);

Podsumowanie

Moje podejście do Anemic Domain Model już znasz. Jestem ciekaw, jak wygląda to u Ciebie. Uważasz to jednoznacznie za antywzorzec czy widzisz dla niego zastosowanie? Jeśli znasz przykład sytuacji, gdy Anemic Domain Model okazał się pomocny lub był źródłem problemu, daj o tym znać w komentarzu. Poniżej znajdziesz też sporo ciekawych materiałów, które pozwolą Ci bardziej zgłębić zagadnienia omówione w tym artykule.

Źródła i materiały dodatkowe

Dominik Szczepaniak

Zawodowo Senior Software Engineer w CKSource. Prywatnie bloger, fan włoskiej kuchni, dobrej kawy i miłośnik jazdy na rowerze.

Inne wpisy, które mogą Cię zainteresować

Kolejna książka o Gicie — naucz się korzystać z Gita jak profesjonalista

Okładka e-booka - Kolejna książka o Gicie

"Kolejna książka o Gicie" to kompleksowy e-book, który pozwoli Ci poznać Gita od A do Z, a także liczne narzędzia dedykowane pracy z Gitem!

Dlaczego warto?

  • 👉 Git od podstaw do poziomu PRO. E-book przeprowadzi Cię krok po kroku, niezależnie od poziomu doświadczenia.
  • 👉 Zdobędziesz praktyczne umiejętności, które natychmiast wykorzystasz w prawdziwych projektach.
  • 👉 Wiedza połączona z praktyką! Oprócz masy teorii w e-booku znajdziesz też zadania praktyczne.

Okładka e-booka - Kolejna książka o Gicie

Przygotuj się lepiej do rozmowy o pracę!

Odbierz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer i realnie zwiększ swoje szanse na rozmowie rekrutacyjnej! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.

Dlaczego warto?

  • 👉 Ponad 1000 pobrań e-booka!
  • 👉 60 stron pełnych pytań i zadań praktycznych. Pytania i zadania pochodzą z faktycznych procesów rekrutacyjnych.

E-booka odbierzesz korzystając z formularza poniżej 👇

Okładka e-booka - Kolejna książka o Gicie

guest

0 komentarzy
Najwięcej głosów
Najnowsze Najstarsze