Blog

10 lipca 2015 Radosław Kłak

Domain Driven Design – Value Objects

Tagi:

Domain Driven Design – Value Objects

W Domain Driven Design podstawowym obiektem są tzw. “Value Objects“. Cóż takiego kryje się pod tą nazwą i dlaczego warto je implementować nawet jeżeli nie podążamy za metodyką Domain Driven Design, z której się one wywodzą? Spróbujemy się pochylić nad zasadami ich tworzenia:

  • Mierzą, określają ilość lub opisują rzeczy,
  • Są niezmienne,
  • Są spójną całością,
  • Są porównywalne,
  • Side-Effect-Free Behaviour.

Podążanie za nimi przyczyni się do lepszej jakości kodu jak i do lepszego zrozumienia aplikacji. Takie obiekty łatwo się tworzy, testuje, używa, optymalizuje i utrzymuje. Zatem zacznijmy od początku!

Mierzą, określają ilość lub opisują rzeczy

Szukając w swojej aplikacji Value Object możesz znaleźć takich kandydatów jak Wiek czy Imię. Wiek nie jest rzeczą, ale raczej opisuje/mierzy ilosć lat konkretnej osoby, podobnie Imię, które nie jest rzeczą, ale nazywa danego człowieka. Można na przykład do tego podejść w ten sposób, że są to brakujące Typy w Twoim języku programowania. Mając typ integer, string itp. dodajemy nowe w postaci klas które, zapewniają nam poprawność danego obiektu. Innym podejściem jest spojrzenie na Value Objects jak na najmniejsze elementy Twojej aplikacji.

Są niezmienne (immutable)

Value Object to taki, którego Wartość jest niezmienna po jego stworzeniu. Jakiekolwiek przypisanie wartości może jedynie zachodzić podczas konstrukcji obiektu. W takim razie jak zmeiniać stan takich obiektów? Często zachodzi potrzeba skorzystania z metody zaczynającej się od set… na danym obiekcie, bo jakaś składowa musi uleć zmianie. W przypadku Value Object takich metod nie chemy! Czytaj dalej…

Są spójną całością

Wyobraźmy sobie Imię, jest to konkretny tekst, którego budowa ma swoje zasady. Możemy przyjąć, że aby móc się posługiwać Imieniem, trzeba mieć tekst, który nie posiada cyfr, spacji, znaków interpunkcyjnych itp. czyli po prostu powinnien się składać z liter. Przykładowa implementacja:

final class Name
{
    /**
     * @var string
     */
    private $name;
    
    /**
     * @param string $name
     */
    public function __construct($name)
    {
        $this->guardName($name);
    
        $this->name = $name;
    }

    /**
     * @return string
     */
    public function __toString()
    {
        return $this->name;
    }

    private function guardName($name)
    {
        if (!is_string($name) || empty($name)) {
            throw new InvalidNameException('Name cannot be empty and must be a string');
        }

        $hasDigits = preg_match('/d+/', $name);
        if ($hasDigits) {
            throw new InvalidNameException('Name cannot have digits');
        }

        $hasInvalidCharacters = preg_match('/[^w,.\'s-]+/u', $name);
        if ($hasInvalidCharacters) {
            throw new InvalidNameException('Name cannot have invalid characters');
        }
    }

}

Oznaczyliśmy klasę jako final, ponieważ nie chemy by po niej dziedziczono, a nigdy nie wiadomo kto po nas zajrzy w ten kod i do jakich pomysłów dojdzie. To jest nasza fundamentalna klasa, w swojej aplikacji też nie dziedziczymy po integer czy string. Spójność naszego obiektu zapewnia strażnik, który odpowiedzialny jest za weryfikacje formatu, jeżeli nie zostanie on spełniony, to rzucamy wyjątkiem. Nie naprawiamy wartości $name, nie filtrujemy, jedynie sprawdzamy czy to co dostaliśmy na wejściu jest OK. Takich strażników może być wiele, w zależności od tego co jest sprawdzane. Jeden Value Object może się składać z innych Value Objects np. klasa Pieniądza, gdzie Waluta(Currency) jest osobnym VO:

final class Money
{
    /**
     * @var int
     */
    private $amount;

    /**
     * @var Currency
     */
    private $currency;

    /**
     * @param int      $amount
     * @param Currency $currency
     */
    public function __construct($amount, Currency $currency)
    {
        $this->guardIntegerNumber($amount);
        $this->guardNonNegativeAmount($amount);

        $this->amount = $amount;
        $this->currency = $currency;
    }

    private function guardNonNegativeAmount($amount)
    {
        if ($amount < 0) {
            throw new InvalidMoneyAmountException('Amount must be positive number');
        }
    }

    private function guardIntegerNumber($amount)
    {
        if (!is_int($amount)) {
            throw new InvalidMoneyAmountException('Amount must be integer number');
        }
    }

    /**
     * @return int
     */
    public function getAmount()
    {
        return $this->amount;
    }

    /**
     * @return Currency
     */
    public function getCurrency()
    {
        return $this->currency;
    }

    ...
}

Jezeli nasz Value Object składa się z kliku właściwości to oznacza to dla nas, że one razem oznaczają już coś nowego.

Są porównywalne

Value Objects tego samego typu są porównywalne ze sobą nie za pomocą ‘==’ czy ‘===’ ale implementuje się własną metodę np. isEqualTo w której możemy sami zdefiniować zasady identyczności dwóch obiektów tego samego typu, np. w Imieniu:

final class Name
{

    /**
     * @var string
     */
    private $name;

    ...

    /**
     * @param Name $other
     *
     * @return bool
     */
    public function isEqualTo(Name $other)
    {
        return $this->name === $other->name;
    }
}

Lub w przypadku Pieniądza, możemy skorzystać z porównania Waluty:

final class Money
{
    /**
     * @var int
     */
    private $amount;

    /**
     * @var Currency
     */
    private $currency;

    ...

    /**
     * @param Money $other
     *
     * @return bool
     */
    public function isEqualTo(Money $other)
    {
        return $this->amount === $other->amount
            && $this->currency->isEqualTo($other->currency);
    }

}

W ten sposób jasno określamy zasady równości i nie musimy pisać w każdym miejscu czy Pieniądz jest równy innemu bazując na np. getAmount, bo możemy zapomnieć o porównaniu Waluty lub innej ważnej reguły w naszym VO.

Side-Effect-Free Behaviour

Wykonanie metody danego obiektu powinno zwrócić wynik nie zmieniając przy tym stanu/własciwości danego obiektu,  czyli można powiedzieć, że mając monetę 5zł i dodasz do niej drugą monetę 5 zł, to nieprzetapiają się one w Twoich rękach i nie zamianiają w banknot 10 zł… Raczej zostawiasz dwie 5 złotówki i wymieniasz je na banknot 10zł. Innymi słowy tworząc w kodzie integer równy 10, a potem przypisując mu wartośc 16, to nie zmieniasz istoty 10tki samej w sobie, tylko masz nową wartość typu integer. Jeżeli podczas wykonywania metody na danym VO, potrzebujesz zmienić jego własciwość to powinno to skutkować nowym obiektem VO, który powinnien zostać zwrócony w wyniku. Oznacza to, że nie modyfikujesz już raz stworzonego VO, tylko otrzymujesz nowe, a co za tym idzie; możesz je porównywać.

final class Money
{
    /**
     * @var int
     */
    private $amount;

    /**
     * @var Currency
     */
    private $currency;

 
    ...

    /**
     * @param Money $money
     * @return Money
     */
    public function add(Money $money)
    {
        if (!$this->currency->isEqualTo($money->getCurrency())) {
            throw new InvalidCurrencyException('Cant add Money with different currency');
        }

        $newAmount = $this->amount + $money->amount;

        return new Money($newAmount, $this->currency);
    }
}

A co z walidacją takich obiektów?

Value Objects zapewniają że ich Wartość raz stworzona jest poprawna/spójna na każdym etapie aplikacji. Tworząc nowe Imię mamy pewność, że jego struktura jest poprawna, ale jeżeli mamy na stronie formularz rejestracji, i chcielibyśmy zwalidować, czy podany Login istnieje już w repozytorium to mówimy już o kontekście, w którym Login VO jest wykorzystywany. Walidacja zachodzi zawsze w jakimś kontekście. Nie powinniśmy implementować tych zasad w samym VO, czyli do konstrukcji przekazywać repozytorium, czy wszystkie możliwe loginy, raczej chcemy by to kontekst miał repozytorium i on weryfikował istnienie takiego Loginu. Staramy się wyciągać takie rzeczy poza obiekt VO. Inny przykład to filtrowanie danych wejściowych one również zachodzą w kontekście, który filtrowanie zapewnia, czyli nim stworzymy obiekt Imienia, to jeżeli mamy obiekcje, że dane wejściowe mogą posiadać spacje na końcu lub na początku, to powinniśmy je usunąć przed stworzeniem takiego obiektu, a nie podczas jego tworzenia.

Podsumowanie

Value Objects to najczęstsze obiekty w Twojej aplikacji i ciągle pojawiają się w dialogu biznesowym. Wszyscy wiedzą, co oznacza Imię, co oznacza Pieniądz i w zależności od Kontekstu wiemy czego oczekujemy. Warto implementować je w swoich projektach nawet jeżeli nie podążamy za metodyką DDD, gdyż idea VO prowadza do bardzo dobrego podziału naszych obiektów na mniejsze przez co łatwiej się nimi posługiwać, utrzymywać, pisać testy i prowadzą do tego, że coraz więcej obiektów pojawia się w Aplikacji, które mają określone zachowania, w przeciwieństwie do wielu miejsc

A jakie Ty masz doświadczenie z Value Objects? Chętnie poznam Twoje komentarze.

 

Na podstawie książki:
Implementing Domain-Driven Design, Vaughn Vernon, 2014

Zobacz na blogu

09.09.2022
Marcin Jahn
It’s Not Just HTTP It’s Not Just HTTP

In today’s world of cloud-based solutions, distributed systems, and microservices-based architectures, network communication is a...

23.08.2022
Adam Mrowiec
Konferencja IPC 2022 Berlin Konferencja IPC 2022 Berlin

Pandemia wreszcie się kończy, dlatego w tym roku postanowiliśmy wrócić do naszych wyjazdów na konferencje....