Skip to content

Refaktoryzacja do wzorca Value Object

Refaktoryzacja ta skutkuje wprowadzeniem w projekcie klas: DriverLicense, Money, Distance oraz Tariff. Każda z nich jest wprowadzona do projektu zupełnie z innego powodu. I tak kolejno:

Każdy z wymienionych tutaj obiektów moglibyśmy scharakteryzować w następujący sposób:

  1. opisuje cechy konkretnego konceptu domenowego — nie sam koncept. Zrealizowany dla klienta przejazd rozpoczął się w pewnym punkcie, skończył w zupełnie innym miejscu. Możemy więc powiedzieć, że dystans jest cechą przejazdu. Sam w sobie, pozbawiony połączenia z przejazdem, jest w zasadzie bezużyteczny. Nie wiadomo, czego ten dystans właściwie dotyczy i czemu miałby służyć.
  2. jest niemutowalny — od momentu utworzenia obiektu nigdy nie podlega on zmianie. Jest niezmienny. Jeśli na obiekcie wywoływana jest metoda, która powinna skutkować zmianą jego stanu, zamiast tego zwracany jest nowy, odpowiednio zainicjalizowany obiekt. Widać to dokładnie na przykładzie metody Money::add. Dzięki temu możemy uniknąć problemów związanych z przypadkowym współdzieleniem obiektu przez inne.
  3. wiąże wiele atrybutów w logiczną całość — obiekt ten modeluje konceptualną całość poprzez powiązanie nawet wielu różnych atrybutów w jedno. Posiadając jedynie informację o stawce za kilometr, nie jesteśmy w stanie określić taryfy w przejeździe. Dopiero połączenie tej stawki z opłatą za rozpoczęcie kursu oraz nazwą, pozwala tego dokonać. Analogicznie, mając informację o dwóch kwotach jednak bez powiązanych z nimi walut, nie jesteśmy w stanie przeprowadzić operacji dodania obu wartości. Jednak gdy potraktujemy kwotę i walutę jako nierozerwalnie złączone ze sobą, dowolna operacja na wartości pieniężnej jest prosta do zrealizowania.
  4. jest łatwo wymienialny z użyciem stabilnego interfejsu — obiekt ten można w bardzo prosty sposób wymienić, zastąpić innym, gdy tylko zmieni się sposób opisu konceptu domenowego. Dostarczymy implementację dla nowej reprezentacji, korzystając z wyznaczonego już stabilnego interfejsu. Rozszerzenie obiektu Money o walutę wymagać będzie zmiany w jednym miejscu, właśnie w tym obiekcie. Oprócz pola z walutą pojawi się także zapewne walidacja związana z dodawaniem kwot w różnych walutach. Reszta systemu nie dowie się jednak o zmianie tych szczegółów implementacyjnych.
  5. porównanie z innym obiektem tego typu następuje poprzez porównanie wartości atrybutów — nie posiada też jednoznacznej tożsamości, wyrażonej na przykład dowolnym identyfikatorem. Dwa utworzone w różnych momentach i miejscach systemu obiekty Distance będą opisywały dokładnie tę samą odległość, jeśli będą przechowywały dokładnie taką samą wartość w polu meters. Tak więc dysponując dwoma przejazdami o tej samej długości, nawet na różnych trasach czy rynkach, moglibyśmy podmienić pomiędzy nimi obiekty Distance i zmiana ta nie wpłynie na logikę działania systemu. Oba obiekty Distance opisują tu dokładnie tę samą cechę przejazdu i mają dokładnie tę samą wartość, a tylko to jest tutaj istotne.

Powyższa charakterystyka opisuje jeden z istotniejszych wzorców implementacyjnych Domain-Driven Design, to znaczy Value Object. Obiekt reprezentujący wartość w naszej domenie. Wprowadzając tego rodzaju obiekt do systemu, zyskujemy zwykle bardzo dużo:

  • odwzorowuje w modelu oprogramowania konkretny element biznesowy,
  • enkapsuluje powiązaną z artybutami logikę, np. walidacji,
  • poprawia czytelność kodu źródłowego,
  • ułatwia wprowadzanie dalszych zmian w systemie, w tym całkowitą wymianę implementacji przy zachowaniu stabilnego interfejsu,
  • sprzyja unikaniu przypadkowych błędów.

Nie wprowadzajmy jednak takich obiektów w każdym możliwym miejscu, gdy tylko zauważymy taką opcję. To zwyczajnie nie ma sensu. Powinny tutaj wystąpić określone powody, dla których konkretny Value Object zostanie zaimplementowany. Gdy pojawia się trudność ze wskazaniem takich powodów lub zysk z takiej refaktoryzacji będzie znikomy, być może wzorzec ten nie będzie najlepszym wyborem.

Zastanów się, czy wprowadzenie w systemie Cabs takich obiektów opisujących kierowcę jak FirstName, czy LastName, będących domenową reprezentacją Stringa, będzie dobrą decyzją i pomoże w dalszym rozwoju. Może wręcz przeciwnie, będzie sztuką dla sztuki i spowoduje na przykład niepotrzebny przerost kodu:

class FirstName {

   String name;

   public FirstName(String name) {
       this.name = name;
   }

   public String getName() {
       return name;
   }

   //...
}

class Driver {

   FirstName firstName;

   public FirstName getFirstName() {
       return firstName;
   }

   public void setFirstName(FirstName firstName) {
       this.firstName = firstName;
   }

   //...
}

Pamiętaj, aby wszystkie decyzje podejmowane były w kontekście konkretnego projektu i jego problemów. Bezpośrednie przenoszenie rozwiązań wypracowanych w zupełnie innych projektach, przy odmiennych założeniach i wymaganiach może dać odwrotny od oczekiwanego efekt. Coś, co w jednej domenie biznesowej może być przedstawione za pomocą Value Objectu, w innej może być już znacznie bardziej złożonym konceptem, wymagającym użycia odmiennych wzorców i technik.

Podsumowanie

Materiały: