<? phpukraine PHP-ПЛАТФОРМА
⌕Пошук по платформі
КОНЦЕПЦІЯ DDD · КРОК 3 З 10

Value Object

Обʼєкт, який важить своїм значенням, а не ідентичністю: вилка зарплати, email, період. Його не змінюють, а замінюють новим.

5 хв читання Junior → Middle
01 · ЩО ЦЕ

Значення, а не запис

Дві купюри по сто гривень однакові: вам байдуже, яку саме дали. З вилкою зарплати від 5 000 до 7 000 доларів так само, важить лише значення. Такий обʼєкт називають value object. На відміну від , у нього немає ID, і два обʼєкти з однаковими полями означають одне й те саме. Це .

СЛОВНИКСутність. Обʼєкт з ідентичністю, яка не змінюється: вакансія лишається тією самою вакансією, навіть коли змінились назва й вилка.
СЛОВНИКРівність за значенням. Два value objects рівні, якщо рівні всі їхні поля. Оператор == у PHP саме так порівнює обʼєкти одного класу.

Value object . Щоб змінити вилку, створюють нову й ставлять на місце старої. Від PHP 8.2 для цього є readonly-класи: властивість не можна перезаписати після конструктора.

СЛОВНИКНезмінність. Після створення стан обʼєкта не змінюється. Метод, який мав би змінити значення, повертає новий обʼєкт.

Правила значення живуть у конструкторі, і це : нижня межа не більша за верхню, сума не відʼємна. Якщо обʼєкт створився, він правильний, і далі код його не перевіряє. Без value object вилка ходить кодом як два int і рядок валюти, а кожна функція перевіряє їх наново або не перевіряє зовсім. Це називають .

СЛОВНИКСамоперевірка. Конструктор відмовляється створити неправильне значення й кидає виняток. Невалідного обʼєкта просто не буває.
СЛОВНИКПримітивна одержимість. Primitive obsession: доменні поняття живуть як int, string і масиви, а правила для них розкидані по коду.
02 · МЕЖА

Значення ходить цілим

newnewsalarySalaryParserRobotaUaSourcevalue objectSalarymidpoint() · format()value objectClassificationсутність, має idJob
newnewsalarySalaryParserRobotaUaSourcevalue objectSalarymidpoint() · format()value objectClassificationсутність, має idJob
Вилка зарплати у вакансіях PHP Ukraine. SalaryParser читає її з тексту, RobotaUaSource з API, і обидва створюють Salary через конструктор, який не пропустить неправильну вилку. Classification тримає Salary як поле, а Job тримає Classification.
Абстракція Ключовий учасник Домен Інфраструктура Клієнт
  • SalaryParser створює Salary.
  • RobotaUaSource створює Salary.
  • Job використовує Classification.
  • Classification використовує Salary (salary).
  1. 1Поля readonly, сетерів немає. Інше значення означає новий обʼєкт.
  2. 2Конструктор перевіряє правила значення. Невалідний обʼєкт не створюється.
  3. 3Поведінка значення живе в ньому: midpoint() і format() належать Salary, а не сервісу.
  4. 4Value object порівнюють за полями. Ідентичність потрібна сутності, яка його тримає.
03 · ЯК У PHP UKRAINE

Вилка зарплати як value object

Salary тримає обидві межі й валюту, а конструктор перевіряє правила. Будь-яка межа може бути відсутня, обидві одразу ні. Код, що отримав Salary, уже знає, що вилка правильна. Коли сторінка вакансії на сайті-джерелі дає вилку, якої не було, Job::enrich() ставить нову класифікацію з Classification::withSalary(), а стару ніхто не змінює.

php Salary.php фрагмент, скорочено
1final readonly class Salary← незмінний
2{
3 public function __construct(
4 public ?int $from,
5 public ?int $to,
6 public Currency $currency,
7 ) {
8 if ($from === null && $to === null) {← правило
9 throw new InvalidArgumentException('Salary needs at least one bound.');
10 }
11
12 if ($from !== null && $to !== null && $from > $to) {← правило
13 throw new InvalidArgumentException('Salary lower bound exceeds the upper one.');
14 }
15
16 if (($from ?? 0) < 0 || ($to ?? 0) < 0) {← правило
17 throw new InvalidArgumentException('Salary cannot be negative.');
18 }
19 }
20
21 public static function range(int $from, int $to, Currency $currency): self
22 {
23 return new self(min($from, $to), max($from, $to), $currency);← новий обʼєкт
24 }
25
26 public function midpoint(): int← поведінка
27 {
28 return (int) round((($this->from ?? $this->to) + ($this->to ?? $this->from)) / 2);
29 }
30}
04 · ТИПОВІ ПОМИЛКИ

Як value objects зазвичай псують

✕ 1

Сетер у value object

setTo() змінює вилку, на яку посилаються два обʼєкти. Зміна в одному місці тихо змінює інше.

Натомість: readonly-клас і метод, що повертає новий обʼєкт, як Classification::withSalary().

✕ 2

Value object з ID

Адресу зберігають окремою таблицею з id і порівнюють за ним. Дві однакові адреси раптом стають різними.

Натомість: зберігайте значення в колонках власника. У Laravel для цього є custom casts, у Doctrine embeddables.

✕ 3

Перевірка поза обʼєктом

Сервіс перевіряє, що нижня межа не більша за верхню, а інший код створює обʼєкт напряму й обходить перевірку.

Натомість: правило в конструкторі. Тоді обійти його неможливо.

05 · ДЕРЕВО РІШЕНЬ

Чи це value object

1
Чи можна замінити обʼєкт іншим з тими самими полями так, що нічого не зміниться?
Дві вилки від 5 000 до 7 000 доларів взаємозамінні.
НІ →Це сутність: їй потрібна ідентичність, як вакансії чи відгуку.
2
Чи має значення власні правила або поведінку?
Межі не переплутані, середина вилки, форматування.
НІ →Вистачить скалярного типу або enum, як Currency.
3
Чи ходять ці поля кодом разом?
Вилку передають разом з валютою, а не трьома окремими аргументами.
НІ →Лишіть примітиви, поки поля не почнуть ходити разом.
ТАК НА ВСІ →Це value object. Зробіть readonly-клас з перевіркою в конструкторі.
06 · СЛОВНИК
Сутність
Обʼєкт з ідентичністю, яка не змінюється: вакансія лишається тією самою вакансією, навіть коли змінились назва й вилка.
Рівність за значенням
Два value objects рівні, якщо рівні всі їхні поля. Оператор == у PHP саме так порівнює обʼєкти одного класу.
Незмінність
Після створення стан обʼєкта не змінюється. Метод, який мав би змінити значення, повертає новий обʼєкт.
Самоперевірка
Конструктор відмовляється створити неправильне значення й кидає виняток. Невалідного обʼєкта просто не буває.
Примітивна одержимість
Primitive obsession: доменні поняття живуть як int, string і масиви, а правила для них розкидані по коду.
07 · ПЕРЕВІР СЕБЕ

Питання зі співбесід

Усі питання з архітектури →

Оператором ==: він порівнює обʼєкти одного класу за значеннями властивостей. Оператор === перевіряє, чи це той самий екземпляр, і для value objects майже ніколи не потрібен. Якщо рівність хитріша, наприклад email без урахування регістру, напишіть метод equals().

Через custom cast: клас з інтерфейсом CastsAttributes перетворює колонки на обʼєкт і назад. Для значення з кількох полів метод set() повертає масив колонок.

Його ідентичність і є значення. Дві вилки з однаковими полями взаємозамінні, тому їх порівнюють за полями, а зберігають у колонках сутності, яка їх тримає.