Предметно-ориентированное проектирование в PHP

в 11:16, , рубрики: DDD, 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.: Статья — всего лишь пища для размышления.

Ссылка на книгу: Эрик Эванс — Предметно-ориентированное проектирование

Автор: follower

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js