Предметно-орієнтоване проектування у PHP

Предметно-орієнтоване проектування у PHP

Стаття, можна сказати, про наболіле.

Через низький поріг входження, звичку до зв'язки з MySQL, відсутність необхідності збирання, відсутність суворої типізації та інших факторів, проекти, написані на PHP, часто не блищать якістю і містять багато нагромаджених запитів у базу, замість красивого чистого коду.

PHP - скриптова мова, сервер відповідає на запит і об'єкти вмирають. Так, це не desktop-додаток.

Але це не означає, що об'єкти предметної області, з якими ми повинні працювати, не потрібні зовсім.

Навпаки! Вони потрібні, вони повинні допомагати нам зберігати і відновлювати їх стан, після їх видалення з пам'яті.

На PHP можна і потрібно писати якісний код, в іншому це взагалі не залежить від мови!

В першу чергу стаття буде корисна для новачків, але думаю не завадить і бувалим розробникам. Можливо, і у вашому проекті все не так, як хотілося б?

Вже 3 роки займаюся розробкою на PHP і постійно мучить одна річ. Замість того, щоб працювати з сутностями в коді, ми працюємо з базою даної. Хоча це неправильно докорінно.

Упевнений, є багато якісних продуктів, але в більшості випадків ситуація досить сумна.

У всіх проектах, з якими доводилося працювати, в коді немає чітко визначених моделей предметної області, з якою потрібно працювати і інкапсуляції, яку потрібно дотримуватися. Усюди одне й те саме.

У проектах низької якості взагалі часто відсутнє поняття «Модель», є тільки заплутана логіка, в коді якої, щось витягують з бази даних, далі величезна купа циклів і умов і потім дані йдуть в шаблон. Більше того, досі живе багато додатків з голими MySQL запитами, в кращому випадку через обгортку над PDO.

У більш якісних проектах використовується ORM, але це не те, з чим хотілося б працювати для реалізації певної функціональності. Все одно, щоб що-небудь зробити, потрібно заглянути в «Модель», подивитися зв'язки, або виконати DESC/EXPLAIN в консолі, з підключеною базою даних.

У результаті код програми не приховує в методах сутності будь-які операції над даними. А код, де повинна бути проста (або не дуже) бізнес-логіка рясніє рядками на кшталт Orm::find.

Така мішанина дуже засмучує. Особливо коли проект великий і переписати неможливо. Максимум - паралельно вести новий код і працювати з ним, а з часом потихеньку відходити від старого.

Після прочитання чудової книги Еріка Еванса «Предметно-орієнтоване проектування» (посилання в кінці статті), або Domain-Driven Design, в голову прийшла думка, що в основному розробка на PHP зводиться на витягуванні рядків з бази даних.

По-правильному, в проекті повинні існувати об'єкти та їх агрегати, через інтерфейс яких можна працювати з сутностями предметної області.

Ми не повинні обходити агрегат і лізти в його підлеглі об'єкти, і точно також не повинні «писати запити», замість того щоб працювати з об'єктами.

Об'єкти повинні вміти зберігати свій стан, а також відновлюватися з БД. Для складних об'єктів і агрегатів можна використовувати фабрики. Але ні в якому разі не можна засновувати всю логіку на mysql-запитах.

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

Наведу дуже простий приклад:

Клієнт - об'єкт-сутність Customer

Замовлення - об'єкт-агрегат «Order»

Одиниця замовлення - незмінне об'єкт-значення «OrderItem»

У клієнта може бути багато замовлень, одне замовлення може складатися з безлічі одиниць.

Одиниця замовлення - незмінне значення, оскільки в разі зміни ціни товару (а також наявності знижки у клієнта), або видалення товару з магазину, історія замовлень повинна бути достовірною, і зберігати в собі дані, які були актуальними на момент покупки. Неправильно просто посилатися на товар.

І так, необхідно написати просту логіку в API-методі/контролері, яка повинна відобразити замовлення користувача.

Код навмисно спрощений і показує тільки саму суть. У даному випадку неважливо яким чином прийшов id, і як ми авторизуємо користувача.

Варіант 1:

$order = Orm::find('Order', [ ['id', $id], ['user_id', $user->getId()] ]);
if (!empty($order)) {
  $items = Orm::find('OrderItem', [ ['order_id', $id] ]);
return $items;
} else {
  return 'Order was not found';
}

Варіант 2:

$order = $user->getOrder($id);
return $order->getItems();

У першому випадку ми йдемо в базу і дістаємо замовлення з запитаним ID у поточного користувача, щоб не видати чуже замовлення,

потім йдемо в базу і дістаємо з таблиці OrderItem записи, які прив'язані до цього замовлення.

Далі скоріше за все потрібно буде «перебрати» результат за допомогою foreach/array_walk/array_map, щоб привести дані в потрібний вигляд.

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

Такі виклики геть руйнують побудовану архітектуру проекту і роблять її просто безглуздою.

Супроводжувати перший варіант коду набагато складніше, і читається він не так легко, як другий варіант. Orm::find c не завжди такими очевидними параметрами і зайва умова.

У разі якоїсь зміни зберігання замовлень в базі даних, нам доведеться по всьому коду шукати і правити виклики ORM, в кращому випадку.

У другому випадку ми маємо яскраво виражені сутності зі зручним інтерфейсом.

У методі getOrder вже вшита логіка перевірки зв'язку замовлення-користувач, у разі чого, наприклад, викинеться Exception. А в методі getItems вже є все необхідне, щоб просто повернути список позицій. Читаючи такий код, відразу зрозуміло, що він власне робить. Крім того, такий код легше тестувати. Можна навіть написати все в один рядок:

return $user->getOrder($id)->getItems();

Висновки:

В один зі своїх проектів, який починав писати інший програміст, по ходу розробки впроваджувати моделі, які не просто інкапсулюють роботу з БД, а ще й ховають недоліки її архітектури і неявні імена полів.

Пишіть код так, щоб вам з ним було приємно працювати надалі і легко супроводжувати.

Раз вже ми пишемо код, використовуючи ОВП, - давайте працювати з об'єктами і використовувати всі переваги цієї парадигми.

Бізнес-логіка яку ми описуємо в проектах, ґрунтується на сутностях, а не на mysql-вибірках і масивах. Не ускладнюйте життя собі та іншим!

Не лінуйтеся написати клас, що описує сутність, там де це необхідно, не лінуйтеся написати метод, який вам знадобиться ще, копіпасть - зло. А з набором готових сутностей і готових методів, подальша розробка, рефакторинг і тестування спроститься і прискориться!

P.S.: Стаття - лише їжа для роздумів.

Посилання на книгу: Ерік Еванс - Предметно-орієнтоване проектування

Image