Стаття, можна сказати, про наболіле.
Через низький поріг входження, звичку до зв'язки з 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.: Стаття - лише їжа для роздумів.
Посилання на книгу: Ерік Еванс - Предметно-орієнтоване проектування