Value Object jest najmniejszym z Building Blocks w Domain Driven Design (DDD). Value Objects pomagają pisać znacznie prostszy kod, który łatwo się testuje, modeluje, utrzymuje i jednocześnie jest niezwykle zrozumiały. W szczególności Value Objects upraszczają walidacje typów prostych (np. int
, string
) które wpuszczamy do systemu. Kolejną dużą zaletą jest prostota implementacji, która pozwala używać Value Objects w każdym projekcie bez konieczności wprowadzenia całego podejścia jakim jest DDD.
Wpis jest częścią serii o DDD.
Właściwości Value Object
Modeluje pojedynczą rzecz w domenie, którą da się zmierzyć, albo ją opisuje – obiektami, które da się zmierzyć są np. temperatura (13°C) albo dystans (1337 metrów). Opisem czegoś w domenie może być imię osoby albo nazwa miasta.
Niezmienny (Immutable) – po utworzeniu takiego obiektu nie można go modyfikować (prywatne metody lub właściwości do ustawiania wartości są używane wyłącznie przez konstruktor). Jeżeli potrzebujemy „ustawić stan” to możemy pokusić o stworzenie metody, która zwraca nowo utworzony Value Object ze zmienioną wartością.
Stanowi integralną całość (whole object) – Value Object może składać się z kilku wartości, które razem są spójne, np. Temperatura składa się z dwóch właściwości 36.6 i jednostki Celsjusz, razem są spójne i dostarczają nam konkretną informacje o temperaturze. Osobno te wartości mogą opisywać cokolwiek i nie mówią o kontekście ich użycia.
Zastępowalny – Jeżeli chcemy zmienić wartość lub opis czegoś, zastępujemy ValueObject innym ValueObject o tym samym typie i nie powoduje to efektów ubocznych (Side-Effects). Zastępujemy, a nie zmieniamy ValueObject.
1
2
3
4
Temperature currentTemperature = new Temperature(36.6, "°C");
// currentTemperature.ChangeTemperature(35.1) - Źle! Nie modyfikujemy ValueObjectów.
currentTemperature = new Temperature(37.2, "°C") // Zastępujemy
- Porównywalność – ValueObjecty są porównywalne do innych instancji o tym samym typie:
1
2
new Temperature(10, "°C") == new Temparature(10, "°C") // true
new Temperature(10, "°C") == new Temparature(10, "°F") // false
- Brak efektów ubocznych (Side-effects free) – metody ValueObjecta mogą zwrócić wynik, ale nie modyfikują stanu ValueObjectu, nie robi nic poza zwróceniem wyniku, czyli nie ma efektów ubocznych.
1
2
Temperature currentTemperature = new Temperature(36.6, "°C");
currentTemperature = currentTemperature.Increase(2.1) // Metoda zwraca wynik bez modyfikowania stanu pierwotnej instacji
Co może być ValueObjectem
Używamy ValueObjectów kiedy interesują nas wyłącznie właściwości (properties) danego obiektu, a nie jego tożsamość (identity). Czyli ValueObjectem nie zrobiliśmy obiektu Person, bo pomimo tego, że dwie osoby mogą mieć to samo imię i nazwisko, to i tak dla nas jest ważniejsze to, że to są dwie różne osoby.
Jak używać
- 🎭Nadawać wymowne nazwy dla samego ValueObjecta jak i dla jego właściwości
- 🛡️ValueObject posiada metody, które walidują samego siebie w konstruktorze, przez co nie jest możliwe stworzenie ValueObject w niepoprawnym stanie, np. obiekt dystans nie może mieć ujemnej wartości
- ValueObject może się składać z kilku typów prostych i innych ValueObjectów
- Pojedynczy, prosty typ może być opakowany ValueObjectem, np. dystans albo Id jest pojedynczą wartością, która nie może by ujemna. Jeżeli zrobimy z niej ValueObjecta i zaczniemy używać go w całym systemie to nie musimy za każdym razem sprawdzać czy wartość jest poprawna, upraszcza to sporą część naszego kodu. Tutaj jednak należy uważać, aby nie przesadzić z ilością ValueObjectów dla pojedynczych wartości, bo może skończyć się to tym, że w wielu miejscach będziemy mieli rzutowanie z typu prostego na ValueObject i w drugą stronę
- 🤝🏼ValueObject może być współdzielony przez wiele bounded contextów (projektów 😉)
- 🍝Unikać wewnątrz ValueObjectu skomplikowanej logiki, która jest potrzebna do jego utrzymywania
Implementacja
Użyjemy wzorca SuperType, aby każdy ValueObject nie musiał powielać kodu związanego z operatorami porównania.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public abstract class ValueObject
{
public static bool operator ==(ValueObject a, ValueObject b)
{
if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
{
return true;
}
if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
{
return false;
}
return a.Equals(b);
}
public static bool operator !=(ValueObject a, ValueObject b)
{
return !(a == b);
}
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
{
return false;
}
var valueObject = (ValueObject)obj;
return GetAtomicValues().SequenceEqual(valueObject.GetAtomicValues());
}
public override int GetHashCode()
{
return GetAtomicValues().Aggregate(1, HashCode.Combine);
}
protected abstract IEnumerable<object> GetAtomicValues();
}
Metoda GetAtomicValues() wymusza, aby klasy pochodne podawały wartości, po których ValueObject ma być porównywalny z innymi instancjami ValueObjecta.
Poniżej przykładowa implementacja ValueObject Temperature:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Temperature : ValueObject
{
public Temperature(decimal value, string unit)
{
Value = value;
SetUnit(unit);
}
public decimal Value { get; private set; }
public string Unit { get; private set; }
private void SetUnit(string unit)
{
if (unit != "°C" && unit != "°F")
{
throw new ArgumentException("Given unit is not supported");
}
Unit = unit;
}
protected override IEnumerable<object> GetAtomicValues()
{
yield return Value;
yield return Unit;
}
}
Powyższy przykład przedstawia obiekt Temperature. Obiekt ten jest Immutable, posiada wewnętrzną walidację wartości, przez co nie można go stworzyć w niepoprawnym stanie i przez nadpisanie metody ValueObject.GetAtomicValues() można porównywać różne instancje pomiędzy sobą. Jeżeli różne instancje mają te same wartości to operator porównania (==) zwróci true.
Podsumowanie
ValueObject to mały, ale potężny building block z DDD, który małym kosztem pozwala na uproszczenie naszej aplikacji, oraz zmniejsza szansę powstania błędu. Można go z powodzeniem stosować bez wdrażania czy też rozumienia reszty DDD. Value Objecty obrazują też najważniejsze cechy programowania obiektowego, takie jak enkapsulacja (inaczej hermetyzacja).
Comments powered by Disqus.