Value Object

17-08-2022

Czy w projekcie masz koncept „pieniądza”? Czy zdarzało Ci się zapomnieć przekazać waluty? A może wszędzie przekazujesz pieniądz jako float?

Na ratunek przychodzi Value Object!

Dlaczego piszę „na ratunek”? Ponieważ pieniądz to nie jest sama liczba. Pieniądz składa się z ilości pieniędzy oraz waluty. To jest nierozłączne.

A w przypadku gdy mamy taki właśnie koncept czegoś to warto w naszym systemie go przedstawić za pomocą Value Object.

Zaznaczę od razu na wstępie, że nie zawsze ma to sens – przykładowo gdy nasz system jest bardzo prosty to może się okazać, że użycie wzorca Value Object to robienie sztuki dla sztuki 😉

Jednakże w większości projektów, gdy widzimy taki koncept to możemy go w systemie przedstawić jako Value Obcject.

Ok, ale czym jest w sumie Value Object?

Obiekt reprezentujący opisowy aspekt dziedziny bez żadnej tożsamości z nim związany

Eric Evans

Niewielki prosty obiekt, taki jak zakres liczb lub dat, którego równość nie zależy od identyczności

Martin Fowler

Brzmi trochę zawile, ale postaram się Ci to przestawić teraz na przykładzie i go sobie omówimy 😉

W PHP już ktoś kiedyś napisał fajną paczkę Money, więc zamiast wymyślać koło na nowo, to wykorzystam tutaj tą bibliotekę. Z klasy Money wybrałam 3 metody.

final class Money
{
    private string $amount;
    private Currency $currency;
    private static string $calculator = BcMathCalculator::class;

    public function __construct(int|string $amount, Currency $currency)
    {
        // ...
    }

    public function equals(Money $other): bool
    {
        if ($this->currency != $other->currency) {
            return false;
        }

        if ($this->amount === $other->amount) {
            return true;
        }

        return $this->compare($other) === 0;
    }

    public function add(Money ...$addends): Money
    {
        // ....
    }

    // ................
}

Brak identyfikatora

Możesz zauważyć w tej klasie, że nie ma identyfikatora („bez żadnej tożsamości” / „nie zależy od identyczności„). To jest pierwsza cecha Value Object. Ujmując inaczej, jak w przypadku rekordów w bazie danych mamy często jego identyfikator, tak przypadku Value Object go nie mamy.

W jaki sposób więc porównujemy czy obiekty są sobie równe?

Poprzez porównywanie pól wartości obiektu, co możesz zobaczyć na przykładzie metody equals. Porównuje ona do siebie walutę oraz ilość pieniędzy.

Niemutowalność

Obiekty niemutowalne to takie których wartości nie zmieniasz. Po pierwszym przypisaniu wartości do zmiennej obiektu, nie możesz ponownie zmienić wartości tej zmiennej. Taka klasa nie posiada setterów.

W jaki sposób w takim razie możesz przeprowadzać operacje na pieniądzu takie jak dodawanie/odejmowanie?

Przy takich operacjach zawsze zwracany jest nowy obiekt klasy Money. Zobacz przykład poniżej gdzie mamy dodawanie pieniądza i na końcu metody ujrzysz „return new self(..)„.

final class Money
{
    // ...

    public function add(Money ...$addends): Money
    {
        $amount = $this->amount;

        foreach ($addends as $addend) {
            if ($this->currency != $addend->currency) {
                throw new InvalidArgumentException('Currencies must be identical');
            }

            $amount = self::$calculator::add($amount, $addend->amount);
        }

        return new self($amount, $this->currency);
    }
}

Value Object to nie tylko dane

Wzorzec Value Object pilnuje poprawności swoich danych, co możesz zobaczyć na przykładzie konstruktora poniżej. Dodatkowo jak już pewnie wcześniej nie uszło Twojej uwadze, może zawierać różne metody, takie jak add z poprzedniego przykładu. Wiec ten obiekt to nie tylko dane, ale też i metody (inne niż getter).

final class Money
{
    private string $amount;
    private Currency $currency;
    private static string $calculator = BcMathCalculator::class;

    public function __construct(int|string $amount, Currency $currency)
    {
        $this->currency = $currency;

        if (filter_var($amount, FILTER_VALIDATE_INT) === false) {
            $numberFromString = Number::fromString((string) $amount);
            if (! $numberFromString->isInteger()) {
                throw new InvalidArgumentException('Amount must be an integer(ish) value');
            }

            $this->amount = $numberFromString->getIntegerPart();

            return;
        }

        $this->amount = (string) $amount;
    }

    // ...........
}

Inne przykłady

Pieniądz jest jednym z głównych przykładów prezentowanych w internecie, ale nie jest to jedyny przykład 😉 Jako Value Object możemy zaprezentować współrzędne geograficzne, datę, zakres dat, zakres liczb, wagę, wielkość, adres (ulica, numer domu, miejscowość, kod pocztowy).

W PHP jest już niemutowalny obiekt reprezentujący datę: DateTimeImmutable.

Czy Value Object używamy tylko przy „nierozłącznych” danych?

NIE.

Jako takie „samodzielne” jednostki możemy podać przykładowo: PESEL NIP, ISBN, e-mail.

final class Pesel
{
    private string $pesel;

    public function __construct(string $pesel)
    {
        if (!$this->isValid($pesel)) {
            throw new \InvalidArgumentException('Invalid PESEL');
        }

        $this->pesel = $pesel;
    }

    private function isValid(string $pesel): bool
    {
        //...

        return $result;
    }

    public function get(): string
    {
        return $this->pesel;
    }
}

Korzyści

Wszystko brzmi intrygująco, ale co w sumie jesteś w stanie zyskać korzystając z tego wzorca?

  1. W głównej mierze konkretne koncepty będą w jednej klasie, co zmniejszy szansę na przekazywanie X zmiennych do różnych klas (w przypadku pieniądza byłaby to waluta oraz ilość). Dodatkowo kod powinien stać się bardziej czytelny i mieć więcej sensu z puntu widzenia biznesowego.
  2. Dodatkowo jak mamy coś w klasie i inne metody korzystają z tego obiektu to nie znają szczegółów tego obiektu w związku z czym jeśli w chwili obecnej przechowujesz w swoim systemie pieniądz jako float, to jeśli podejmiesz decyzję o zmianie na inny typ, przykładowo int, aby trzymać w najmniejszej jednostce (grosze) to zmienisz to w stosunkowo małej ilości miejsc. Dlaczego małej, a nie, że w jednej? Należy pamiętać, że oprócz obiektu do rozważenia zostaje sposób zapisu do bazy danych. Czy tam pozostanie zapis złotówek czy jednak groszy? Ale nadal zmiana ta będzie mniej dotkliwa niż jakbyśmy w całym systemie przekazywali po prostu zmienną $money jako float.
  3. Kolejną korzyścią jest to, że jeśli zmieni się sposób walidacji, to masz jedno miejsce do modyfikacji, bo klasa ta pilnuje swojej poprawności. Wyobraź sobie, że za 2 lata (w teorii) zmieni się sposób walidacji PESEL, albo NIP, w ilu miejscach w Twoim systemie będzie trzeba wprowadzić zmianę? A jakby był użyty wzorzec VO (Value Object) to zmianę wprowadzisz w jednym miejscu 😉
  4. No i ostatnim aspektem jest CV Driven Development 😉 Zawsze możesz wpisać, że znasz trochę Domain-Driven Design, bo właśnie z tego wywodzi się Value Object 🙂

Code Smells

Inaczej nazywane „brzydkie zapaszki w kodzie”, „zapachy kodu”. Ogólnie o Code Smells myślę, że napiszę osobny wpis (a może i nawet kilka, bo jest trochę tych zapaszków), ale wydaje mi się, że warto poruszyć jeden zapaszek w kontekście Value Object.

Ale najpierw, czym jest Code Smell?

A code smell is a surface indication that usually corresponds to a deeper problem in the system. The term was first coined by Kent Beck while helping me with my Refactoring book.   […]

Firstly a smell is by definition something that’s quick to spot – or sniffable as I’ve recently put it.  […]

The second is that smells don’t always indicate a problem. Some long methods are just fine. You have to look deeper to see if there is an underlying problem there – smells aren’t inherently bad on their own – they are often an indicator of a problem rather than the problem themselves.

Martin Fowler

Primitive Obsession

Jednym z przykładów takiego zapaszku jest „Primitive Obsession„. Jest to używanie nadmiarowo pierwotnych typów języka programowania zamiast obiektów, przykładowo: float / int / string. W naszym przykładzie z pieniędzmi byłoby to przekazywanie do każdej metody parametru $money jako float. Innym przykładem byłoby przekazywane do różnych metod numeru PESEL jako string, zamiast obiektu. Tak więc Value Object jest jednym ze sposobów pozbycia się tego zapaszku 😉

UWAGA

  1. Fowler w swojej książce zaznacza, że w środowisku osób korzystających z technologii J2EE wzorzec Value Object jest określany nazwą „Data Transfer Object” (DTO). W późniejszym czasie w tej samej społeczności zaczęto używać nazwy „Transfer Object” zamiast „Value Object”. Piszę o tym tutaj, ponieważ uważam, że warto mieć na uwadze to, że w niektórych społecznościach ten wzorzec może być nazywany inaczej, co może prowadzić do pewnych nieporozumień. Autor książki zaznacza również, że DTO jest innym bytem niż Value Object.
  2. W książce Evansa znajdziemy informację, że w pewnych sytuacjach możemy się zdecydować na to, aby VO były jednak mutowalne (mogły zmieniać swoje wartości). Dodaje też, że są to wyjątkowe sytuacje i powinno się ich unikać. Sytuacją w której autor książki daje drobne przyzwolenie na mutowalnośc obiektów to wtedy gdy ich niemutowalność zacząco wpływa na wydajność aplikacji.
  3. Nie zawsze coś co wydaje się, że będzie Value Object nim będzie. Wszystko zależy od kontekstu biznesowego. We wcześniejszej części napisałam, że przykładem VO jest adres. Jednakże w książce Evans podaje dwa przykłady gdzie w jednym faktycznie adres możemy przedstawić jako Value Object, a w drugim przykładzie będzie to Encja (identyfikowalny adres).

W dużym skrócie DTO, VO i Encję (Entity) możemy porównać tym zestawieniem:

Encja Value Object DTO
ma dane
TAK TAK TAK
ma logikę
TAK TAK nie
ma identyfikator
TAK nie nie

 

Ten artykuł był dla Ciebie wartościowy? Podziel się nim!

Feedback bardzo mile widziany 😉

 


Źródła

 

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.