Read, Write, Partial Update Наступна відома проблема, коли ми | Beer::PHP 🍺

Read, Write, Partial Update

Наступна відома проблема, коли ми намагаємось використовувати один і той самий обʼєкт як для читання (наприклад представлення на frontend) так і для запису (створення, оновлення).

Візьмемо іншу сутність в рамках нашого створення замолення — Customer. Побудуємо його з низки Value Objects щоб гарантувати, що дані завжди валідні.

$customer = new Customer(
new CustomerId(uniqid()),
new Email($request->email),
new Name($request->name)),
new Password($request->password))
);

$repository->save($customer);

Які ж можуть виникнути проблеми?

Щось, що ми не хочемо відображати

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

interface CustomerRepository
{
public function save(Customer $customer): void;
public function get(string $email): Customer;
}

Наприклад захешований пароль


{
"customerId": "5f2b...",
"email": "jane@doe.com",
"name": "Jane Doe",
"password": "$2y$10$..."
}


Так, можемо по місцю зробити щось на кшталт:

final readonly class Customer
{
// ...

public function serialize(): array
{
return [
'email' => $this->email,
'name' => $this->name,
];
}
}

Зміна правил валідації

Їдемо далі. Наші перевірки в Value Objects можуть змінюватись з часом. В якийсь момент, ми можемо зробити їх більш жорсткими. В базі даних можуть зберігатись записи, котрі можуть не пройти правила нової валідації, якщо ми будемо використовувати один і той самий обʼєкт для читання і для запису. Тут починаються трюки з Reflection. Вже не так райдужно.

Додаткові дані для відображення

Наступна проблема, коли для відображення нам потрібна інформація, котра не міститься в поточному обʼєкті. Наприклад, кількість замовлень.

[
...$customerRepository->getById($customerId),
...['orders_count' => $orderRepository->count($customerId)]
]

Варіант робочий, проте ми тут робимо 2 запити до бази даних, хоча потенційно можна зробити один.

Чи дійсно створення та оновлення це одне і те саме?

При створенні нового Customer ми очікуємо email, name та password. Але, наприклад, при зміні імені, що ми маємо передавати в полі пароля. Чи навпаки, при зміні пароля нам потрібна специфічна логіка (запит на зміну, надсилання токена тощо).

interface CustomerRepository
{
public function save(Customer $user): void;
public function update(Customer $user): void;
}

// Updating name
$customer = new Customer(
$request->email,
$request->name,
'а що тут робити з паролем?',
);

$repository->update($customer);

Щоб вирішити ці проблеми, доцільно використовувати окремі моделі для різних операцій. Наприклад:

final readonly class CustomerRead
{
public function __construct(
public CustomerId $customerId,
public Email $email,
public Name $name,
public int $orderCount,
) {}
}

interface CustomerReadRepository
{
public function get(CustomerId $customerId): CustomerRead;
}

Або для оновлення даних:

final readonly class CustomerNameUpdate
{
public function __construct(
public CustomerId $customerId,
public Name $name,
)
}

interface CustomerRepository
{
public function save(Customer $customer): void;
public function updateData(CustomerNameUpdate $customerNameUpdate): void;
public function updatePassword(CustomerPasswordUpdate $customerPasswordUpdate): void;
}

Write model забезпечує дотримання інваріантів і консистентність даних. Оскільки тут ми не паримось як саме будемо виводити ці дані - це дозволяє нам робити обʼєкт маленьким і конкретним.

Read model дає можливість отримати всю необхідну інформацію, потрібну для конкретного відображення чи перегляду.

Не бійтеся створювати окремі представлення для різних операцій. Ваш код стане більш стабільним, логічни та простим для підтримки.

#backend #architecture #middle #source
Beer::PHP 🍺

Beer::PHP 🍺

@beerphp
2.01K Подписчиков
Технологии Категория
Тут публікуються короткі замітки про PHP, Linux, Unit Testing, DB, OOP тощо, витяги зі статей, книг, відео, курсів та інших ма...