Value Objects в PHP: перестаньте писать код, одержимый примитивами
Посмотрите почти на любую кодовую базу PHP, и вы найдёте один и тот же паттерн: email хранится как строка, деньги — как float, ID — как целое число, номера телефонов — как строки. Это называется primitive obsession (одержимость примитивами) — чрезмерное использование примитивных типов для представления доменных концепций. Это один из самых распространённых и самых разрушительных антипаттернов в современной PHP-разработке.
Value Objects — это лекарство. Они приносят типобезопасность, валидацию и ясность в ваш код — и как только вы начнёте их использовать, вы удивитесь, как жили без них раньше.
Что такое Value Object?
Value Object (VO, объект-значение) — это небольшой иммутабельный объект, который представляет концепцию из вашего домена. В отличие от Entity, Value Object не имеет идентичности — он полностью определяется своими значениями. Два Value Object с одинаковыми значениями считаются равными.
Классические примеры включают:
- Email — вместо сырой строки
- Money — вместо float плюс строка валюты
- UserId — вместо целого числа
- PhoneNumber — вместо строки
- DateRange — вместо двух объектов DateTime
- Address — вместо массива строк
- Password — вместо простой строки
Проблема с примитивами
Рассмотрим этот типичный код:
Каждое примитивное значение несёт скрытые допущения. Система типов ничего не говорит вам о том:
- Валидно ли значение
- В каком оно формате
- Какие операции разрешены
- Какие единицы или валюту оно представляет
- Можно ли его безопасно изменить или переиспользовать
Результат? Логика валидации разбросана повсюду, баги из-за несовпадающих форматов и код, о котором трудно рассуждать.
Ваш первый Value Object: Email
Заменим примитивную строку на полноценный Value Object:
Теперь система типов защищает вас. Если у вас есть объект Email, вы знаете, что он валиден. Больше никаких разбросанных проверок валидации.
Использование Value Object
Более сложный пример: Money
Деньги — классический случай, когда примитивы терпят катастрофу. Float теряет точность, а смешивание валют — это тихий баг, который только и ждёт своего часа.
Теперь посмотрите, насколько чистым становится вызывающий код:
Ключевые характеристики хорошего Value Object
- Иммутабельный — будучи созданным, он никогда не меняется. Операции возвращают новые экземпляры.
- Самовалидирующийся — экземпляр не может существовать в невалидном состоянии.
- Без идентичности — равенство основано на значениях, а не на ID в базе данных.
- Без побочных эффектов — никаких запросов к базе данных, логирования, глобального состояния.
- Маленький — представляет одну концепцию, а не целый агрегат.
- Заменяемый — весь объект можно заменить, а не мутировать поле за полем.
Value Object vs. DTO vs. Entity
Важно различать эти три паттерна:
- Value Object — иммутабельный, определяется значениями, содержит поведение, связанное с этими значениями (например,
add()у Money). - DTO — иммутабельный или мутабельный, переносит данные между слоями, не содержит бизнес-логики.
- Entity — имеет уникальную идентичность, которая сохраняется во времени, даже когда её атрибуты меняются.
Где Value Objects блистают
- Доменные модели — представление email, денег, ID, дат, адресов.
- Валидация ввода — создание VO из пользовательского ввода гарантирует валидность везде дальше по цепочке.
- Бизнес-правила — инкапсуляция логики, такой как сопоставление валют или проверка диапазонов дат.
- Тестирование — более простые, быстрые и сфокусированные тесты без зависимостей от базы данных.
- Рефакторинг — когда требования меняются, вы меняете VO в одном месте.
Распространённые ошибки, которых стоит избегать
- Добавление сеттеров — это нарушает иммутабельность. Если нужно другое значение, создайте новый экземпляр.
- Делать их слишком большими — VO должен представлять одну концепцию, а не целый агрегатный корень.
- Добавление логики персистентности — держите заботы о базе данных в репозиториях, а не в VO.
- Игнорирование равенства — всегда реализуйте метод
equals(). - Выброс общих исключений — определяйте доменно-специфичные исключения для более ясной обработки ошибок.
Практический путь рефакторинга
Вам не нужно переписывать всё приложение за одну ночь. Начните здесь:
- Определите примитивное поле, которое вызывает баги или путаницу (email, деньги, ID).
- Создайте для него Value Object с валидацией и полезными методами.
- Замените примитив в одном классе или одном модуле.
- Позвольте компилятору и вашим тестам направлять остальное.
- Повторите со следующим очевидным кандидатом.
Заключение
Value Objects — один из самых высокоэффективных рефакторингов, которые вы можете сделать в кодовой базе PHP. Они устраняют одержимость примитивами, централизуют валидацию, делают невозможным представление невалидных состояний и превращают ваш код в самодокументируемое выражение вашего домена. Система типов становится союзником, а не формальностью.
Начните с одного Value Object — может быть, Email или Money. Как только вы увидите, сколько ясности это приносит в ваш код, вы никогда не вернётесь к передаче сырых строк.
Перестаньте писать код, одержимый примитивами. Позвольте вашим типам говорить за ваш домен.