PHP

Делаю ToDoLog - часть 3. Response

Просмотров: 5618Комментарии: 8
Технологии

С концепцией запроса в целом разобрался, что делать с результатами работы Интерактора? В каком формате возвращать результат, как учесть ошибки и валидацию данных? Думаю, что Интерактор должен возвращать объект Ответа. Мотивация аналогична объекту Запроса: четкая структура ответа и типизация, более абстрактный Интерактор не должен заботиться о конкретном формате возвращаемых данных, требуемом клиентской стороной. С другой стороны, клиент будет знать структуру возвращаемых данных и сможет с ними работать.

Следующий вопрос: как возвращать объект Ответа? Вижу два основных варианта. В первом объект создается внутри интерактора и после заполнения данными отправляется наружу. Во втором в интерактор передается пустой (установленный по умолчанию) объект ответа, который внутри заполняется данными. Какой вариант предпочтительнее? Это зависит от того, что нужно делать в контроллере с этим объектом. Если понадобится расширять и дополнять его, то предпочтителен второй вариант. Такая возможность полезна, ведь одни и те же данные часто нужно возвращать на клиент в разном виде. Поэтому для контроллеров может понадобиться дополнительная функциональность, облагораживающая данные для показа в конкретном шаблоне.

Сначала я написал реализацию с созданием объекта ответа внутри интерактора (коммит). Базовый класс Response содержит поля для статуса ответа и ошибок, если таковые возникнут. Плюс геттеры и сеттеры для статуса. Это общая функциональность, которая нужна всем классам-наследникам. В последних задаются конкретные поля для возвращаемых данных.

По некотором размышлении подумалось, что правильнее использовать второй вариант - создание чистого объекта ответа на клиенте (в контроллере) и передача его для заполнения в интерактор. Основную роль в этом убеждении сыграли вышеприведенные соображения плюс соображения о предпочтении интерфейсов реализациям. Так и переделал.

И еще. В интеракторе создания задачи разделил само по себе создание объекта-задачи и добавление его в репозиторий (коммит). По-хорошему, создание объекта и сохранение его не связаны между собой по смыслу. Не всегда созданный объект нужно сохранять, можно, например, вернуть его клиенту в ответ на запрос. Поэтому добавление созданного объекта будет производиться при помощи метода Repo\TaskInterface::add(Entity\Task $task) . За создание объекта отвечает метод Repo\TaskInterface::create(...) (коммит). Он позволяет избежать явного создания Сущности там, где она нужна.

Итак, использование отдельного объекта Ответа унифицирует формат возвращаемых в контроллер данных и отвязывает эти данные от типа ответа клиенту.

Делаю ToDoLog - часть 2. Request

Просмотров: 2297Комментарии: 0
Технологии

Один из недостатков кода - механизм обработки данных внешних запросов. Сейчас данные новой задачи передаются в Интерактор обычным массивом, внутри дополняются, модифицируются, потом из них строится Сущность. Проверяется наличие нужных полей, заполняются значения по-умолчанию, и вот они уже образовали новый объект-задачу. Главной проблемой такого подхода является априорное знание Сущности и Интерактора о поступивших данных - что это массив, какие поля он содержит. По сути, в этом нет ничего плохого, должен же Интерактор знать, что ему нужно для работы? Проблема в том, что нужно знать о формате входных данных. Представьте, что они приходят не из массива $_POST , а от запроса через API, или из командной строки. А если поле из $_POST - массив, а шлюз к API возвращает объект? Нужно либо предусмотреть в Интеракторе или Сущности обработку по-разному организованных входных данных, либо заранее привести входные данные к известному формату. Первое нарушает правило Clean Architecture - более абстрактные слои не должны зависеть от менее абстрактных. Второе приемлемо: можно привести "сырой" запрос к стандартному виду, откуда бы он не поступил, а уже "причесанный" отправить для обработки в Интерактор. В качестве такого "причесанного" можно задействовать массив с известными полями, но более адекватным, на мой взгляд, будет использование объекта с уникальными свойствами для каждого типа запроса (коммит). Для объектов в PHP легче задавать структуру, можно требовать передачи в функцию параметра определенного класса, что автоматически определяет его сигнатуру. Возможность добавлять и переопределять методы-обработчики тоже не будет лишней.

При таком подходе передача в конструктор сущности Task массива параметров становится бессмысленной. Нужно передавать отдельные поля. Вопрос только в том, все ли поля передавать сразу в конструктор. Нормально ли, если объект появится без поля userId, например? И когда считать объект корректным и валидировать его корректность? Пока буду передавать в конструктор все, что нужно, и формировать корректный объект (коммит).

И, конечно, нужно аналогично переделать создание сущности User. Интеракторов для нее пока нет, так что в сценарии передаем параметры вручную (коммит).

Резюме: теперь интерактор не знает о том, откуда получен запрос и какая его внутренняя структура. Получает нужные данные в стандартном виде.

Делаю ToDoLog - часть 1

Просмотров: 3183Комментарии: 4
Технологии

В прошлый раз я озадачился двумя вопросами о Clean Architecture: как работать со структурами данных типа дерева и какими все же должны быть Сущности. Подумав и почитав ветки, пришел к следующему.

Структура данных Сущности должна отображаться либо в ней самой, либо в специально созданной другой сущности. Но при этом служебным данным в них не место. Например, если нужно иметь дерево задач, я могу эмулировать его свойством children, куда помещать задачи-непосредственные потомки. Или же создать отдельную сущность TaskTree, которая будет содержать иерархию Task. Думаю, на теперешнем этапе более приемлем первый вариант.

public $children = [];

Разумеется, должны добавиться методы, обрабатывающие эту иерархию, в нашем случае addChild() , removeChild() и другие. Чтобы не грузить каждый раз всю иерархию, можно добавить поле $hasChildren , которое будет показывать, есть ли у задачи потомки.

Далее. Как я понял из комментариев, Сущность в первую очередь должна определять бизнес-правила обработки себя. Заполнение, выборка, валидация — все, что работает на высшем, самом абстрактном уровне иерархии приложения. При этом непосредственного доступа к данным она может и не иметь. То есть, Сущность может и должна быть абстрактным классом, от которой наследуются конкретные реализации с ORM, например. Такое усложнение и вызвано, по-видимому, наличием слоя ORM, который сам работает с БД, но при этом должен реализовать требуемые бизнес-методы сущности. Это говорит о том, что у меня все неправильно. Впрочем, ломать сейчас не буду. Есть частные мнения, что сущности все же могут быть конкретными классами с нормальными полями данных, формироваться из данных БД они могут в Репозиториях. Кроме того, хочу, чтобы изменения в коде были естественными, а пока не вижу необходимости менять реализацию класса.

На сегодняшний день реализован сценарий создания задачи.

$taskCreator = new Interactor\Task\Creation($this->taskRepo, $this->userRepo, $this->sessionService);
$this->boolResult = $taskCreator->execute($data);

При создании в него инжектируются репозитории задачи и пользователя, а также сервис для работы с сессией. Для выполнения он получает на вход данные новой задачи, которые дополняет ID создателя и вызывает метод создания задачи в репозитории.

$data['userId'] = $this->session->getLoggedInUserId();
$this->taskRepo->create($data);

В чем достоинство такого кода? Создание задачи отделено от обслуживающего вызов кода (в данном случае, теста). Бизнес-логика реализуется внутри Интерактора, только он знает, какие поля выбрать, куда записать и как сохранить. Но при этом абстрагируется от конкретного способа сохранения — сейчас за это отвечает mock-репозиторий , работающий с оперативной памятью.

Недостатков у кода много, о них пока не буду. Интересно посмотреть, как такой код может взаимодействовать с запросами извне.

Начал работу над пробным проектом Clean Architecture

Просмотров: 2627Комментарии: 0
Технологии

Начал писать код приложения (простой ToDo-менеджер-журнал). Пока что все просто, не знаю, стоит ли что-то пояснять. Описал первые сценарии, вырисовалась структура для Сущностей/Интеракторов/Границ. Предполагается, что на первых порах сценарии будут работать с фиктивными (mock) источниками данных и сервисами. Работу с сессией реализовал в виде сервиса, а не репозитория, поскольку хранение данных текущего пользователя возможно не только в сессии, но и, например, с использованием токенов. Идея сервиса для обработки таких вещей кажется более уместной.

Столкнулся и с первой архитектурной проблемой. Набор задач логично организовать в виде дерева, с иерархией подзадач. Но как отобразить ее в нашей Сущности задачи? С одной стороны, чего проще — ввести дополнительные поля для работы с Nested Sets или Adjacency List. Но эти поля будут отображением реляционной таблицы в нашу Сущность. А Сущность не должна знать о внутреннем устройстве системы хранения данных! Стал копать гуглогруппу о Clean Architecture - и запутался еще больше. Оказывается, Сущность не должна быть отображением нашего объекта данных.

Misconception #1: Entities are data.
Entities are not data. Entities are objects, and objects have functions that implement business rules. Entities may _use_ data; but the _are not_ data. Entities are behavior.

Еще один удар в рушащуюся картину мира нанес следующий пункт

Misconception #2: Entities, Database, and Web use the same data
They most definitely do _not_. Databases store data in strange and arcane formats (like tables) that is inconvenient for most business rule calculations. For that reason we often use ORMs to map the tables into more convenient data structures. Entities use the data from those data structures; but the mapping is not one-to-one. Very often a given entity will use more than one of those data structures. ... The mapping is complex. Moreover, most entities only want certain data elements, and not the entire data structure. So, to keep the entities from knowing too much, it is wise to map the generic data structures that come from the database into entity-specific data structures that keep the entities decoupled from data they don't need.
...
Be very careful. The temptation to use the same data structures (even the same objects) throughout the entire system is strong at first, and can lead you into a coupling nightmare. Keep these tiers separate. Allow the structure of the data in each tier to conform to the needs of _that_ tier.

На этом я пока остановился. Нужно собраться с мыслями и понять, как же должны работать Сущности здорового программиста.

Clean Architecture на PHP

Просмотров: 2948Комментарии: 0
Технологии

Посмотрел видео Robert C. Martin об подходе к построению веб-приложений, обеспечивающем такие преимущества хорошей архитектуры, как

  1. возможность отложить принятие важных архитектурных решений
  2. возможность минимизировать количество принимаемых на каждом этапе архитектурных решений

В печатном виде изложение можно почитать на 8thlight.com. Если кратко, суть подхода в максимальной изоляции бизнес-логики приложения от фреймворка/сервисов/среды выполнения. Сердцевиной приложения являются Сущности (Entities), которые содержат самую общую и высокоуровневую бизнес-логику системы. Уровнем ниже лежат Сценарии (Use Cases), или Взаимодействия (Interactors), связывающие данные Сущностей для реализации бизнес-логики приложения. Ни этот, ни более высокий уровень не должны зависеть от тленных земных вещей типа пользовательского интерфейса, библиотек фреймворка или сервиса базы данных. Интерфейсные адаптеры (Interface Adapters), или Границы (Boundaries), обеспечивают взаимодействие между бизнес-логикой и окружением. Тут работают шлюзы, сервисы БД и внешних API и прочая. Именно с этим слоем взаимодействует фреймворк, если его использовать в разработке. Границы преобразуют потоки данных из внешнего мира в пригодные для обработки высокоуровневым ядром, и наоборот, возвращают от ядра ответ в браузер, UI или куда еще. Основное правило при всем этом - более высокий уровень абстракции не должен зависеть от более низкого уровня.

Изоляция бизнес-логики приложения кроме возможности гибкой работы с окружением позволяет плотно и на самых ранних этапах разработки использовать тестирование. В идеале, можно написать и покрыть тестами систему, ничего еще не зная о фреймворке, базе данных и даже о том, будет ли приложение работать в вебе или на десктопе с планшетом.

В теории все это заманчиво и воодушевляет, но насколько реально сделать такую штуку в коде? Не разобьется ли эта неземная красота, эти почти Платоновы идеи-сущности, о грубость техзадания? Интересно было бы посмотреть варианты реализации чего-нибудь как на Ruby, например, так и на кондовом PHP. Лучшее из того, что я видел по теме, это цикл статей Jeremy Bush aka zombor (между прочим, именно из этого цикла и узнал о сабже). К сожалению, рассказ прервался на самом интересном и до рабочего кода не дошел. На Гитхабе тоже негусто. Не знаю, будет ли время, но интересно было бы написать приложение в подобном стиле.

Что вы думаете о Clean Architecture в реальной разработке? Может быть, даже случалось делать красивую архитектуру?