Пишем Value object на PHP

Value object - это тип, обертывающий данные и отличающийся только своими свойствами. В отличие от Entity, у него нет уникального идентификатора. Таким образом, два value objects с одинаковыми значениями свойств следует считать равными.

Хорошим примером кандидатов на объекты значений являются:

  • номер телефона
  • адрес
  • цена
  • хэш коммита
  • идентификатор сущности
  • и так далее.

При проектировании объекта-значения следует обратить внимание на три его основные характеристики: неизменность, структурное равенство и самопроверка.

Вот пример:

declare(strict_types=1);

final class Price
{
    const USD = 'USD';
    const CAD = 'CAD';

    /** @var float */
    private $amount;

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

    public function __construct(float $amount, string $currency = 'USD')
    {
        if ($amount < 0) {
            throw new \InvalidArgumentException("Amount should be a positive value: {$amount}.");
        }

        if (!in_array($currency, $this->getAvailableCurrencies())) {
            throw new \InvalidArgumentException("Currency should be a valid one: {$currency}.");
        }

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

    private function getAvailableCurrencies(): array
    {
        return [self::USD, self::CAD];
    }

    public function getAmount(): float
    {
        return $this->amount;
    }

    public function getCurrency(): string
    {
        return $this->currency;
    }
}

Неизменность (Immutability)

Как только создается экземпляр value object, он должен быть одинаковым для остальной части жизненного цикла приложения. Если нужно изменить его значение, это должно быть сделано путем полной замены этого объекта.

Использование изменяемых объектов значений (mutable value objects) приемлемо, если использовать их полностью в локальной области, только с одной ссылкой на объект. В противном случае могут возникнуть проблемы.

В предыдущем примере показано, как можно обновить количество типа Price:

declare(strict_types=1);

final class Price
{
    // ...

    private function hasSameCurrency(Price $price): bool
    {
        return $this->currency === $price->currency;
    }

    public function sum(Price $price): self
    {
        if (!$this->hasSameCurrency($price)) {
            throw \InvalidArgumentException(
                "You can only sum values with the same currency: {$this->currency} !== {$price->currency}."
            );
        }

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

Структурное равенство (Structural Equality)

Объекты значений (Value objects) не имеют идентификатора. Другими словами, если два объекта значений имеют одинаковые внутренние значения, их следует считать равными. Поскольку в PHP нет способа переопределить оператор равенства, его нужно реализовать его самостоятельно.

Можно создать специализированный метод для этого:

declare(strict_types=1);

final class Price
{
    // ...

    public function isEqualsTo(Price $price): bool
    {
        return $this->amount === $price->amount && $this->currency === $price->currency;
    }
}

Другой вариант - создать хэш на основе его свойств:

declare(strict_types=1);

final class Price
{
    // ...

    private function hash(): string
    {
        return md5("{$this->amount}{$this->currency}");
    }

    public function isEqualsTo(Price $price): bool
    {
        return $this->hash() === $price->hash();
    }
}

Самопроверка (Self-Validation)

Проверка объекта значений должна происходить при его создании. Если какое-либо из его свойств недопустимо, оно должно выдать исключение. Объединяя это с неизменяемостью, после создания объекта значений можно быть уверены, что он всегда будет действительным.

Если еще раз взять пример с типом Price, не имеет смысла иметь отрицательную сумму для домена приложения:

declare(strict_types=1);

final class Price
{
    // ...

    public function __construct(float $amount, string $currency = 'USD')
    {
        if ($amount < 0) {
            throw new \InvalidArgumentException("Amount should be a positive value: {$amount}.");
        }

        if (!in_array($currency, $this->getAvailableCurrencies())) {
            throw new \InvalidArgumentException("Currency should be a valid one: {$currency}.");
        }

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

Заключение

бъекты значений полезны для написания чистого кода. Вместо того, чтобы писать:

public function addPhoneNumber(string $phone): void {}

Можно написать:

public function addPhoneNumber(PhoneNumber $phone): void {}

Это упрощает чтение и рассуждения об этом, также не нужно выяснять, какой формат телефона должен быть использован.

Поскольку их атрибуты определяют их, и можно поделиться ими с другими различными объектами, они могут кэшироваться вечно.

Они могут помочь вам уменьшить дублирование. Вместо кратных полей amount и и currency вы можете использовать чистый класс Price.

Конечно не стоит злоупотреблять value objects. Представим преобразование тонны объектов в примитивные значения, чтобы сохранить их в базе данных, или преобразуем их обратно в объекты значений при извлечении их из базы данных. Действительно, в таком случае могут быть проблемы с производительностью. Кроме того, наличие множества детализированных объектов-значений может раздуть кодовую базу.

С помощью объектов значений можно уменьшить одержимость элементарными типами. Их использование нужно для представления поля или группы полей домена, которые требуют проверки или могут вызвать неоднозначность, если используются примитивные значения.

Написать комментарий