Encja, pojemnik na dane czy zachowania?

Z jednej strony wiemy o encji będącej reprezentacją wiersza z tabeli bazy, a z drugiej o miejscu na logikę biznesu. Encja może mieć tylko zmapowane pola bazy czy coś więcej? Dzisiaj o tym, kiedy lepiej mieć encje bogate, a kiedy anemiczne.


Entity

Pojęcie encji może być rozpatrywane na kilka sposobów. Większości z nas słowo encja kojarzy się głównie z bazą danych. Jest to poprawne skojarzenie, ponieważ encja jest odwzorowaniem identyfikowalnego obiektu w modelu, np. bytem reprezentującym wiersz z tabeli bazodanowej. Encje opisywane są atrybutami i co najważniejsze posiadają tożsamość. Poprzez tożsamość rozumie się pewną unikatową wartość lub grupę wartości, która pozwala zidentyfikować konkretną instancję na tle zbioru. Podczas omawiania konceptu Value Object wspominałem, że nie posiadają one tożsamości, czyli wykluwa nam się już jedna z głównych różnic pomiędzy tymi wzorcami. 

Model anemiczny (anemic model)

Wspomniałem jednak o rozpatrywaniu encji na kilka sposobów. W istocie, możemy patrzeć na ten koncept z kilku różnych płaszczyzn. Przywykliśmy chyba do tego, że nasza encja w kodzie posiada skończony zbiór właściwości oraz propertkę tożsamującą, np. uuid. Dodatkowo, jeżeli używamy ORM, to uświadczymy mechanizmy bindujące konkretne pola klasy z polami w bazie danych, a także jeśli encja posiada relacje, to zobaczymy pola zawierające złączone dane. Może jeszcze jakiś setter, getter, a także konstruktor? Jeżeli tak wyglądają Twoje encje to znaczy, że używasz tzw. encji anemicznych.

Reprezentacja encji sprowadzająca się jedynie do mapowania wartości: propertka klasy - pole w bazie, a operacje biznesowe na tych danych są wyniesione poza samą encję, np. do serwisów lub (co gorsza) do kontrolerów, powoduje, że narażamy się na posiadanie trudnego w utrzymaniu kodu, brak dobrej widoczności reguł biznesu czy zwyczajnie: niepoprawne rozmieszczenie odpowiedzialności biznesowej.

@Entity('meals')
export class Meal {
  @PrimaryColumn()
  id: string;

  @Column({ type: 'enum', enum: MealType })
  type: MealType;

  @Column({ type: 'enum', enum: MealStatus })
  status: MealStatus;

  setStatus(status: MealStatus): void {
    this.status = status;
  }
}

export class MealsService {
  async changeMealStatus(mealId: string, status: MealStatus): Promise<void> {
    const meal = await this.mealRepository.findOneOrFail(mealId);

    if (meal.type === MealType.breakfast && status === MealStatus.skipped) {
       throw new RuntimeException('Cannot skip the most important meal!');
    }

    if (meal.status === status) {
       throw new RuntimeException('Already ${status}.');
    }

    if (meal.status !== MealStatus.new) {
       throw new RuntimeException('Already ${meal.status}.');
    }

    meal.setStatus(status);

    await this.mealRepository.save(meal);
  }
}

Model bogaty (rich model)

Poprawne miejsce na logikę zmiany statusu w powyższym przykładzie nie jest w serwisie, a w obiekcie reprezentującym konkretny posiłek. W tym przypadku to posiłek wie czy może zostać pominięty, czy może zostać zjedzony. Wyodrębnienie tej logiki do innego miejsca skutkuje mało przejrzystym interfejsem, niepotrzebną granulacją odpowiedzialności biznesowej czy ogólnym ukryciem skutków polecenia changeMealStatus(), mógłbym tak wymieniać i wymieniać.

Lekarstwem na anemię jest... bogactwo. No, może nie w prawdziwym życiu, ale w przypadku encji na pewno. Czym jest to tzw. bogactwo i model bogaty? Jest to posiadanie logiki związanej strikte z daną encją w niej samej. Mamy wtedy wysoką spójność, brak problemów z interfejsem i reguły biznesu są tam, gdzie powinny być. Zaraz, zaraz, a co z serwisami? Nie potrzebujemy ich? Potrzebujemy, ale ich zadanie troszeczkę się zmienia i o serwisach w kontekście bardziej domenowym powiem już wkrótce, a powyższy przykład możemy poprawić następując:

@Entity('meals')
export class Meal {
  @PrimaryColumn()
  private id: MealId;

  @Column({ type: 'enum', enum: MealType })
  private type: MealType;

  @Column({ type: 'enum', enum: MealStatus })
  private status: MealStatus;

  // entity constructor()

  eat(): void {
    if (this.status !== MealStatus.new) {
       throw new RuntimeException('Cannot eat already ${this.status} meal.');
    }

    this.status = MealStatus.eaten;
  }

  skip(): void {
    if (this.status !== MealStatus.new) {
       throw new RuntimeException('Cannot skip already ${this.status} meal.');
    }

    if (this.type === MealType.breakfast) {
       throw new RuntimeException('Cannot skip the most important meal!');
    }

    this.status = MealStatus.skipped;
  }
}

export class MealsService {
  async skipMeal(mealId: string): Promise<void> {
    const meal = await this.mealRepository.findOneOrFail(mealId);

    meal.skip();

    await this.mealRepository.save(meal);
  }

  async eatMeal(mealId: string): Promise<void> {
    const meal = await this.mealRepository.findOneOrFail(mealId);

    meal.eat();

    await this.mealRepository.save(meal);
  }
}

Czy anemic entity jest zawsze złe?

Dochodzimy do sedna sprawy encji. Pytanie nad którym głowią się wszyscy filozofowie świata: encja bogata czy anemiczna? Dawno nie pisałem, że to zależy, ale dzisiaj to zależy nie od frameworka czy technologii, ale od problemu, który musisz rozwiązać. Jeśli implementujesz CRUD-a, to nie ma najmniejszego sensu tworzenia modeli bogatych, bo i po co? W przypadku prostych krudzików nie ma na tyle (albo i żadnej) logiki związanej strikte z encją. Sprawa ma się inaczej, jeżeli na Twoich encjach zachodzą operacje takie, jak np. ta powyżej. Im więcej logiki związanej z danym modelem, tym bardziej powinieneś/powinnaś zastanowić się nad modelem bogatym.

czym różni się encja bogata od anemicznej 

Jeśli chcesz wiedzieć jaki model powinien zostać zastosowany ZANIM zaczniesz kodować, to zachęcam Cię do zaznajomienia się z koncepcją Domain Driven Design, o której już niejednokrotnie wspominałem.

Podsumowanie





Polecane wpisy:

Wzorzec stan (state)

Dziś prezentuję kolejny wzorzec projektowy, który pozwala uniknąć drabinek if-else if. Mowa tutaj o wzorcu state, który idealnie nada się, jeśli posiadasz różne stany w swoim systemie oraz chcesz mieć możliwość płynnego przechodzenia pomiędzy nimi.

Sprawdź ten wpis

Wzorzec pełnomocnik (virtual proxy)

Wzorzec virtual proxy służy do tworzenia obiektów pośrednich, które nadzorują dostęp do obiektów dla których są pełnomocnikami. Dzięki temu, że pełnomocnik i klasa bazowa udostępniają jednakowy interfejs, możemy ukryć w pośredniku dodatkową logikę.

Sprawdź ten wpis

Wzorzec strategia (strategy pattern)

Jeżeli masz dość ifologii w swoim kodzie, to konieczne sprawdź czym jest czynnościowy wzorzec projektowy strategia. Pozwala on mądrze obsługiwać różne scenariusze w procesie i jednocześnie być fancy pod względem zasad SOLID.

Sprawdź ten wpis

Wzorzec adapter

Strukturalny wzorzec projektowy adapter to spore udogodnienie w walce z systemami legacy. Pozwala użyć niepasującego interfejsu w innym. Dzięki temu zabiegowi pomimo, iż nie posiadamy spójnej abstrakcji możemy zastosować pożądaną logikę.

Sprawdź ten wpis

Wzorzec dekorator

Strukturalny wzorzec projektowy dekorator, to lekarstwo na problemy z mnogością różnych podtypów. Pozwala on na dynamiczne generowanie kolejnych typów obiektów bazujących na pierwotnym typie, bez konieczności deklarowania nowych, dedykowanych klas.

Sprawdź ten wpis

Autor wpisu:

Gabriel Ślawski

Fanatyk czystego i prostego kodu. Zwolennik podejść DDD oraz Modular Monolith. Na codzień pracuje jako programista i architekt. Po godzinach spełnia się w projektach open source, udziela się na blogach oraz czyta książki o kosmosie i astrofizyce.