Value Object (VO) to jeden z kluczowych wzorców projektowych w programowaniu obiektowym, który świetnie sprawdza się w PHP oraz w architekturze DDD (Domain-Driven Design). Choć jego znaczenie często pozostaje nieco niedocenione, jego wdrożenie może poprawić jakość kodu, zrozumiałość aplikacji i bezpieczeństwo danych.
W tym artykule wyjaśnię, czym jest Value Object, jakie są jego cechy oraz jak go stosować w PHP.
Czym jest Value Object?
Value Object, jak sama nazwa wskazuje, to obiekt, który reprezentuje wartość. W przeciwieństwie do tzw. obiektów encji, które identyfikujemy po ich tożsamości (ID), Value Object jest identyfikowany przez swoją wartość. Przykłady typowych VO to adresy, waluty, pieniądze czy odległości. Te obiekty mają sens tylko wtedy, gdy ich wartość jest poprawna i identyczna z oczekiwaną wartością.
Na przykład, „100 PLN” to wartość, którą można przedstawić jako Value Object, ponieważ liczy się tu wyłącznie wartość i waluta, a nie żadne ID, które mogłoby identyfikować ten obiekt.
Kluczowe cechy Value Object
➡️ Niepowtarzalność tożsamości
VO nie jest identyfikowane przez tożsamość, ale przez swoją wartość. Dwa obiekty VO o tej samej wartości są traktowane jako identyczne.
➡️ Niezmienność
Raz stworzony VO nie powinien być modyfikowany. Zamiast tego, gdy chcemy zmienić jego wartość, tworzymy nowy obiekt. Dzięki temu mamy pewność, że wartość VO nie ulegnie zmianie w nieoczekiwany sposób.
➡️ Zachowanie zasad jednolitości
VO przechowuje wartości w sposób zgodny z założeniami biznesowymi. Na przykład, klasa waluty będzie miała tylko te wartości, które są zgodne z systemem walutowym (np. trzy litery waluty, kwota nieujemna).
➡️ Prosta równość wartości
VO można łatwo porównywać za pomocą ich wartości – dwa obiekty z identyczną wartością uznajemy za równe.
Dlaczego warto stosować Value Object?
Wdrożenie VO do architektury aplikacji niesie za sobą kilka zalet:
- Bezpieczeństwo i spójność danych: VO pozwala na precyzyjne definiowanie wartości, zapobiegając w ten sposób niepożądanym modyfikacjom danych.
- Poprawa czytelności kodu: VO jasno definiuje, co reprezentuje dany obiekt, co znacząco poprawia zrozumienie kodu i przyspiesza jego utrzymanie.
- Łatwość testowania: Ze względu na niezmienność VO są idealnymi kandydatami do testów jednostkowych, ponieważ ich stan nie zmienia się w czasie.
Przykład Implementacji Value Object w PHP
Załóżmy, że mamy aplikację, która obsługuje ceny w różnych walutach. Możemy zdefiniować Value Object dla klasy Money, który przechowuje kwotę oraz walutę:
final class Money { private float $amount; private string $currency; public function __construct(float $amount, string $currency) { if ($amount < 0) { throw new InvalidArgumentException("Kwota nie może być ujemna"); } if (!in_array($currency, ['USD', 'EUR', 'PLN'], true)) { throw new InvalidArgumentException("Nieprawidłowa waluta"); } $this->amount = $amount; $this->currency = $currency; } public function getAmount(): float { return $this->amount; } public function getCurrency(): string { return $this->currency; } public function equals(Money $other): bool { return $this->amount === $other->amount && $this->currency === $other->currency; } }
Omówienie kodu
Konstruktor
W konstruktorze sprawdzamy poprawność danych, aby upewnić się, że żadne nieprawidłowe wartości nie zostaną przypisane do Money. Przykładowo, zapobiegamy przypisaniu ujemnej wartości dla kwoty oraz wymuszamy użycie określonych kodów walut.
W Value Objects (VO) walidacje często dodaje się w konstruktorze, ponieważ konstruktor jest miejscem, gdzie tworzy się obiekt i można zagwarantować, że jego stan będzie poprawny już od momentu powstania. Value Objecty powinny być niezmienne, co oznacza, że po utworzeniu ich wartości nie powinny się zmieniać. Dlatego ważne jest, aby sprawdzać poprawność danych już w momencie ich inicjalizacji, aby uniknąć późniejszych problemów z nieprawidłowymi danymi.
Dodanie walidacji w konstruktorze ma kilka zalet:
- Niezmienność: Value Object jest tworzony tylko raz i nie zmienia się po utworzeniu. Walidacja w konstruktorze sprawia, że nie musisz sprawdzać poprawności danych za każdym razem, kiedy używasz tego obiektu – masz pewność, że są one poprawne.
- Ograniczenie błędów: Jeśli wartość nie przejdzie walidacji, nie zostanie utworzony obiekt. Pozwala to unikać błędów wynikających z operacji na niepoprawnych danych.
- Jednolitość stanu: Każdy Value Object, który został utworzony, jest od razu w pełni poprawny i zgodny z wymaganiami biznesowymi.
- Kapsułkowanie logiki biznesowej: Konstruktor staje się miejscem, gdzie można umieścić logikę dotyczącą danego typu danych. Przykładowo, jeśli tworzymy obiekt reprezentujący numer telefonu, można w konstruktorze upewnić się, że numer spełnia wymagania dla formatu telefonicznego.
Innymi słowy, walidacja w konstruktorze pomaga utrzymać zasady DDD i Clean Code, zapewniając poprawność danych w VO od samego początku jego istnienia.
Metoda equals()
Metoda pozwala na porównanie dwóch obiektów Money. Jeżeli kwota i waluta są takie same, dwa obiekty Money są uznawane za identyczne.
W przypadku Value Objects (VO), nadpisywanie i stosowanie metody equals (oraz często hashCode) jest bardzo ważne, ponieważ VO są definiowane przez swoje wartości, a nie przez swoją tożsamość (tak jak ma to miejsce w przypadku encji).
Głównymi powodami, dla których stosuje się equals w Value Objects to:
- Porównywanie wartości, a nie referencji: Value Objects nie mają unikalnego identyfikatora, który odróżniałby je od siebie nawzajem. Oznacza to, że dwa obiekty VO są traktowane jako identyczne, jeśli ich wszystkie atrybuty są takie same, nawet jeśli są to różne instancje w pamięci. Metoda equals pozwala na porównanie zawartości tych obiektów, a nie ich referencji.
- Ułatwienie pracy z kolekcjami: Kiedy VO są przechowywane w strukturach danych, takich jak Set czy Map, metoda equals umożliwia rozróżnienie lub identyfikację obiektów na podstawie ich wartości. Jeśli dwa obiekty VO mają takie same wartości, Set traktuje je jako duplikaty (z uwagi na ich “wartościową” równość).
- Logika biznesowa i spójność: Dzięki implementacji equals można w prosty sposób porównać dwa obiekty VO i sprawdzić, czy reprezentują tę samą wartość biznesową. Przykładowo, jeśli VO reprezentuje pieniądze, equals pozwala łatwo porównać, czy dwie kwoty są sobie równe.
- Testy i czytelność kodu: Stosowanie equals ułatwia pisanie testów jednostkowych i porównywanie wartości w logice aplikacji, co zwiększa czytelność i zmniejsza ryzyko błędów wynikających z porównywania referencji zamiast zawartości.
Equals w Value Objects pozwala na porównywanie instancji na poziomie wartości, co jest kluczowe dla ich użycia w modelu domenowym, zapewniając zgodność z ideą niezmienności i braku tożsamości VO.
Niezmienność
Po ustawieniu wartości amount i currency obiekt Money nie może być zmieniony – możemy jedynie odczytać jego wartości.
Zobaczmy jeszcze inny przykład
W aplikacjach często operujemy na adresach e-mail, ale ich poprawność i format muszą być zachowane na każdym kroku. Value Object EmailAddress pozwala enkapsulować tę logikę, zapewniając poprawny i bezpieczny sposób zarządzania danymi typu e-mail.
Zajrzyjmy do implementacji:
final class EmailAddress { private string $email; public function __construct(string $email) { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException("Invalid email address."); } $this->email = $email; } public function getEmail(): string { return $this->email; } public function equals(EmailAddress $other): bool { return strtolower($this->email) === strtolower($other->getEmail()); } public function __toString(): string { return $this->email; } }
Dlaczego EmailAddress jako VO?
- Walidacja w konstruktorze: Już na etapie tworzenia obiektu EmailAddress sprawdzamy, czy wartość jest poprawnym adresem e-mail, co zmniejsza ryzyko błędów w dalszej części aplikacji.
- Niezmienność: Obiekt EmailAddress jest niezmienny — raz utworzony adres e-mail nie może być zmieniony. Gwarantuje to spójność i bezpieczeństwo danych.
- Równość wartości: Metoda equals umożliwia porównywanie dwóch adresów e-mail, ignorując wielkość liter. Dzięki temu mamy pewność, że różne instancje z tą samą wartością są traktowane jako równoważne.
- Łatwa konwersja do tekstu: Metoda __toString() pozwala na bezpośrednie użycie obiektu w miejscach wymagających reprezentacji tekstowej, co jest praktyczne w wielu sytuacjach.
Jak korzystać z Value Object?
Gdy chcemy korzystać z VO, powinniśmy starać się używać ich wszędzie tam, gdzie liczy się wyłącznie wartość, a nie unikalna tożsamość. Poniżej przykładowe użycie dla pierwszego przykładu:
$price = new Money(100.0, 'PLN'); $discountedPrice = new Money(100.0, 'PLN'); if ($price->equals($discountedPrice)) { echo "Obie wartości są identyczne."; } else { echo "Wartości są różne."; }
oraz dla adresu email:
$email1 = new EmailAddress("[email protected]"); $email2 = new EmailAddress("[email protected]"); if ($email1->equals($email2)) { echo "Email addresses are the same."; } else { echo "Email addresses are different."; }
Value Object EmailAddress pomaga utrzymać integralność danych i ułatwia pracę z e-mailami, unikając wielokrotnej walidacji w różnych częściach aplikacji.
W jakich sytuacjach unikać VO?
Mimo licznych zalet, Value Object nie jest idealnym rozwiązaniem w każdej sytuacji. Lepiej unikać VO tam, gdzie tożsamość obiektu jest kluczowa (np. identyfikatory użytkowników, zamówienia), ponieważ w takich przypadkach najważniejsze jest nie to, co reprezentuje obiekt, ale jego unikalność i ciągłość.
Podsumowanie
Value Object to niezwykle użyteczny wzorzec projektowy, który pomaga organizować i przechowywać dane w spójny, bezpieczny sposób. Dzięki identyfikacji przez wartość, niezmienności oraz łatwości testowania, VO może znacząco usprawnić nasz kod, szczególnie w bardziej rozbudowanych projektach, gdzie dbamy o skalowalność i przejrzystość.
Stosując VO, zyskujemy bardziej intuicyjny i zrozumiały kod, który lepiej oddaje złożoność naszej domeny.
Nikt jeszcze nie komentował. Bądź pierwszy!