Wzorce projektowe #1: Value Object

Założeniem tego bloga jest pisanie głównie o dobrych praktykach. Pisząc o nich nie sposób jest nie wspomnieć o wzorcach projektowych. W kolejnych postach będę starał się opowiadać o różnych wzorcach w jak najprzystępniejszy sposób.

Pierwszy wzorzec, o którym opowiem jest dość prosty, ale bardzo ważny. Każdy wzorzec projektowy ma za zadanie rozwiązać jakiś powszechny programistyczny problem (jeśli odpowiednio go zastosujemy). Zacznę więc od sformułowania pewnego przykładowego problemu.

Opis problemu

Poniżej napisałem przykładowy kod raportu, w którym spisywane są wszystkie wpływy danego dnia. Raport bierze kwoty netto, dodaje do nich VAT i tworzy listę wszystkich wpływów.

final class DailySalesReport {
  (...)

  public function formatInvoicePriceGross(
    float $netPrice
  ) : string {
    $netPrice = $netPrice * 1.23;
    return number_format(
      $netPrice, 2, ',', ' '
    ) . ' PLN';
  }
}

Funkcja jest dość prosta. Najpierw dodaje do podanej kwoty 23% podatku VAT, po czym drukuje ją w odpowiednim formacie z walutą na końcu. Problem pojawia się w momencie kiedy firma zaczyna przyjmować płatności w różnych walutach. Rozwiązanie tego problemu jest jednak dość proste.

Rozwiązanie proste

final class DailySalesReport {
  (...)

  public function formatInvoicePriceGross(
    float $netPrice, 
    string $currency
  ) : string {
    $netPrice = $netPrice * 1.23;
    return number_format(
      $netPrice, 2, ',', ' '
    ) . ' ' . $currency;
  }
}

Problem rozwiązany, ale czy na pewno? Należy się jeszcze upewnić, że format waluty jest poprawny.

final class DailySalesReport {
  (...)

  public function formatInvoicePriceGross(
    float $netPrice, 
    string $currency
  ) : string {
    if (!$currencyValidator->isValid($currency)) {
      throw new InvalidCurrencyException($currency);
    }

    $netPrice = $netPrice * 1.23;
    return number_format(
      $netPrice, 2, ',', ' '
    ) . ' ' . $currency;
  }
}

Jak widzimy całość zaczyna się dość mocno komplikować. Nie dość, że wszędzie musimy przekazywać conajmniej jeden dodatkowy parametr, to jeszcze jesteśmy zmuszeni w każdym miejscu upewniać się, czy wartości są poprawne. Coś takiego nie mieści się w zakresie odpowiedzialności tego prostego raportu, a całość systemu się nam niepotrzebnie komplikuje. Co zatem możemy zrobić?

Value Object

Wzorzec Value Object służy nam do przechowania jakiejś wartości, którą ciężko będzie nam przedstawić za pomocą zwykłego skalara. W tym przypadku chcemy przedstawić pieniądze, ale wraz z walutą potrzebujemy conajmniej dwóch typów prostych. Zdefiniujmy więc klasę, która posłuży nam do przechowywania wartości pieniężnej.

final class Money {
  /** @type float */
  private $value;

  /** @type string */
  private $currency;

  public function __construct(
    float $value, 
    string $currency
  ) {
    if (!$this->isCurrencyValid($currency)) {
      throw new InvalidCurrencyException($currency);
    }

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

  public function getFormattedValue() {
    return number_format(
      $this->value, 2, ',', ' '
    ) + ' ' + $this->currency;
  }

  public function multiply(float $multiplier) : void {
    $this->value = $this->value * $multiplier;
  }
}

Nasz obiekt enkapsuluje nasze dwa typy proste, oba przekazywane są przez konstruktor. To bardzo ważne! Przez konstruktor zawsze przekazujemy wszystkie składniki, którą są nam potrzebne do utworzenia poprawnego obiektu. Dzięki temu chronimy się przed powstawaniem wadliwych obiektów klasy Money, np. nie posiadających ustawionej waluty. W konstruktorze też upewniamy się czy waluta jest poprawna, eliminując konieczność sprawdzania tego w innych miejscach w kodzie.

Posiadając taką klasę możemy w niej umieścić wszystkie funkcje związane z wartością pieniężną. To znacznie uporządkuje nasz kod. Po zastosowaniu wzorca kod funkcji będzie prezentował się następująco:

final class DailySalesReport {
  (...)

  public function formatInvoicePriceGross(
    Money $netPrice
  ) : string {
    $netPrice->multiply(1.23);
    return $netPrice->getFormattedValue();
  }
}

Jest jednak jeden poważny problem. Zaimplementowaliśmy ten wzorzec w sposób nie właściwy. Musimy zadbać o jeszcze jedną rzecz.

Niezmienność / Immutability

Należy zauważyć, że kiedy używamy metody multiply, to zmieniamy wartość obiektu. Obiekt ten do funkcji trafił najprawdopodobniej przez referencję, prowadzącą prosto do np. faktury. Zmieniając ten obiekt możemy przypadkowo zmienić kwotę całej faktury! Przyjętą zasadą jest, że każdy Value Object powinien być niezmienny (immutable). Oznacza to, że raz ustawionej wartości nigdy nie zmieniamy w danej instancji obiektu, a w razie potrzeby zmiany tworzymy po prostu nową instancję. Jak to wpłynie na nasz kod?

final class Money {
  (...)

  public function multiply(float $multiplier) : Money {
    $newValue = $this->value * $multiplier;
    return new Money($newValue, $this->currency);
  }
}

Należy też wprowadzić kilka zmian do naszej funkcji:

final class DailySalesReport {
  (...)

  public function formatInvoicePriceGross(
    Money $netPrice
  ) : string {
    $grossPrice = $netPrice->multiply(1.23);
    return $grossPrice->getFormattedValue();
  }
}

Value Object w PHP

Przy okazji warto wspomnieć o tym, że bardzo popularny w PHP obiekt DateTime również ma swój niezmienny odpowiednik DateTimeImmutable. Warto rozważyć jego wykorzystanie podczas pisania kolejnych operacji na datach. Pozwala to uniknąć wielu wyrwanych włosów z głowy 😉

Jeśli podobał Ci się ten wpis, daj mi o tym znać i obserwuj tego bloga. Będę jeszcze duuużo pisał o różnych wzorcach projektowych. Jeśli coś z tego co napisałem jest niejasne, albo co gorsza błędne, daj mi proszę znać, służę pomocą.

4 thoughts on “Wzorce projektowe #1: Value Object

  1. Fajny post, przyjemnie się czyta, przykładowy kod bardzo pomocny, z niecierpliwością czekam na kolejne wpisy 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *