Что такое Value Object (Объект-Значение)?

«Что такое Value Object (Объект-Значение)?» — вопрос из категории ООП, который задают на 31% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Value Object (VO, Объект-Значение) — это небольшой неизменяемый объект, который представляет собой составное значение. Его ключевая особенность: равенство определяется не по идентификатору, а по совпадению значений всех его свойств.

Основные характеристики, которые я реализую:

  • Неизменяемость (Immutable): После создания состояние объекта нельзя изменить. Любая операция, которая должна "изменить" значение, возвращает новый экземпляр VO.
  • Отсутствие идентичности: Нет поля id. Два VO с одинаковыми значениями свойств считаются полностью взаимозаменяемыми.
  • Самодостаточность и валидация: Объект создается только с валидными данными. Валидация происходит в конструкторе.

Практический пример: Денежная сумма с валютой.

final class Money
{
    // Свойства только для чтения гарантируют неизменяемость
    public function __construct(
        private readonly int $amountInCents, // Храню в копейках/центах для точности
        private readonly string $currencyCode // ISO код, например 'USD', 'EUR'
    ) {
        // Валидация при создании
        if ($amountInCents < 0) {
            throw new InvalidArgumentException('Amount cannot be negative.');
        }
        if (!in_array($currencyCode, ['USD', 'EUR', 'GBP'], true)) {
            throw new InvalidArgumentException("Unsupported currency: {$currencyCode}");
        }
    }

    // Операция возвращает НОВЫЙ объект
    public function add(Money $other): self
    {
        if ($this->currencyCode !== $other->currencyCode) {
            throw new InvalidArgumentException('Cannot add different currencies.');
        }
        return new self($this->amountInCents + $other->amountInCents, $this->currencyCode);
    }

    // Сравнение по значению, а не по идентичности
    public function equals(Money $other): bool
    {
        return $this->currencyCode === $other->currencyCode 
               && $this->amountInCents === $other->amountInCents;
    }

    // Геттеры для получения значений
    public function getAmount(): float { return $this->amountInCents / 100; }
    public function getCurrencyCode(): string { return $this->currencyCode; }
}

// Использование
$salary = new Money(500000, 'USD'); // 5000.00 USD
$bonus = new Money(100000, 'USD');  // 1000.00 USD
$total = $salary->add($bonus); // Новый объект Money(600000, 'USD')

$moneyA = new Money(1000, 'EUR');
$moneyB = new Money(1000, 'EUR');
var_dump($moneyA->equals($moneyB)); // bool(true) - они равны по значению

Где я это использовал: Для представления Email, URL, географических координат, диапазонов дат. Это предотвращает "примитивную одержимость" (Primitive Obsession) и делает код типобезопасным и выразительным.