Значення, а не запис
Дві купюри по сто гривень однакові: вам байдуже, яку саме дали. З вилкою зарплати від 5 000 до 7 000 доларів так само, важить лише значення. Такий обʼєкт називають value object. На відміну від , у нього немає ID, і два обʼєкти з однаковими полями означають одне й те саме. Це .
Value object . Щоб змінити вилку, створюють нову й ставлять на місце старої. Від PHP 8.2 для цього є readonly-класи: властивість не можна перезаписати після конструктора.
Правила значення живуть у конструкторі, і це : нижня межа не більша за верхню, сума не відʼємна. Якщо обʼєкт створився, він правильний, і далі код його не перевіряє. Без value object вилка ходить кодом як два int і рядок валюти, а кожна функція перевіряє їх наново або не перевіряє зовсім. Це називають .
Значення ходить цілим
- SalaryParser створює Salary.
- RobotaUaSource створює Salary.
- Job використовує Classification.
- Classification використовує Salary (salary).
- 1Поля readonly, сетерів немає. Інше значення означає новий обʼєкт.
- 2Конструктор перевіряє правила значення. Невалідний обʼєкт не створюється.
- 3Поведінка значення живе в ньому: midpoint() і format() належать Salary, а не сервісу.
- 4Value object порівнюють за полями. Ідентичність потрібна сутності, яка його тримає.
Вилка зарплати як value object
Salary тримає обидві межі й валюту, а конструктор перевіряє правила. Будь-яка межа може бути відсутня, обидві одразу ні. Код, що отримав Salary, уже знає, що вилка правильна. Коли сторінка вакансії на сайті-джерелі дає вилку, якої не було, Job::enrich() ставить нову класифікацію з Classification::withSalary(), а стару ніхто не змінює.
Як value objects зазвичай псують
Сетер у value object
setTo() змінює вилку, на яку посилаються два обʼєкти. Зміна в одному місці тихо змінює інше.
Натомість: readonly-клас і метод, що повертає новий обʼєкт, як Classification::withSalary().
Value object з ID
Адресу зберігають окремою таблицею з id і порівнюють за ним. Дві однакові адреси раптом стають різними.
Натомість: зберігайте значення в колонках власника. У Laravel для цього є custom casts, у Doctrine embeddables.
Перевірка поза обʼєктом
Сервіс перевіряє, що нижня межа не більша за верхню, а інший код створює обʼєкт напряму й обходить перевірку.
Натомість: правило в конструкторі. Тоді обійти його неможливо.
Чи це value object
- Сутність
- Обʼєкт з ідентичністю, яка не змінюється: вакансія лишається тією самою вакансією, навіть коли змінились назва й вилка.
- Рівність за значенням
- Два value objects рівні, якщо рівні всі їхні поля. Оператор == у PHP саме так порівнює обʼєкти одного класу.
- Незмінність
- Після створення стан обʼєкта не змінюється. Метод, який мав би змінити значення, повертає новий обʼєкт.
- Самоперевірка
- Конструктор відмовляється створити неправильне значення й кидає виняток. Невалідного обʼєкта просто не буває.
- Примітивна одержимість
- Primitive obsession: доменні поняття живуть як int, string і масиви, а правила для них розкидані по коду.
Питання зі співбесід
Оператором ==: він порівнює обʼєкти одного класу за значеннями властивостей. Оператор === перевіряє, чи це той самий екземпляр, і для value objects майже ніколи не потрібен. Якщо рівність хитріша, наприклад email без урахування регістру, напишіть метод equals().
Через custom cast: клас з інтерфейсом CastsAttributes перетворює колонки на обʼєкт і назад. Для значення з кількох полів метод set() повертає масив колонок.
Його ідентичність і є значення. Дві вилки з однаковими полями взаємозамінні, тому їх порівнюють за полями, а зберігають у колонках сутності, яка їх тримає.