Hexagonal Architecture / 3.2. Добавляем API списка и создания мастера
Сервисный слой
В предыдущей части мы рассмотрели доменный слой нашей функциональности. Настало время переходить к сервисному слою.
Организация кода и интерфейсов
В сети множество авторов предлагают совершенно разные схемы организации кода. Одни организуют код в виде классов сервисов, которые потом используются в контроллерах и других адаптерах. Другие предлагают все пользовательские сценарии бить на интерфейсы помещать их в пакет с названием *.port.in, а исходящие порты помещать в виде интерфейсов в пакет *.port.out, что явно указывает на входящие и исходящие порты.
Наша структура и организация кода будет выглядеть следующим образом:
В последнее время становится широко распостраненным подход, в котором функционал создания и изменения отделяется от фукнционала чтения, классами, интерфейсами или сервисами (да зравствует CQRS!). Будем использовать этот подход. Разместим в корне пакета интерфейсы, для основных бизнес-сценариев нашего приложения - CreateExpertCommandRequest, GetAllExpertsQuery, GetServiceItemsAvailableForExpertQuery. Здесь *Comand обозначает, что сценарий подразумевает модификацию данных, а *Query - что данные запрашиваются для представления пользователю.
Здесь GetAllExpertsQuery, GetServiceItemsAvailableForExpertQuery нужны лишь как вспомогательные сценарии, подробно на них останавливаться не будем, потому что в дальнейшем они вероятнее всего претерпят изменения. Данные интерфейсы были введены с целью проверки основного сценария создания Эксперта.
Пакет *.impl содержит реализацию интерфейсов команд и запросов. В пакете *.port содержатся иходящие интерфейсы (порты), необходимые нашим службам для работы. Пакет request содержит запросы (DTO) необходимые службам, чтобы выполнить свою работу. CreateExpertCommandRequest - содержит необходимую информацию, которую команда CreateExpertCommand будет использовать для создания Эксперта.
Бизнес сценарий (Сервис/Служба)
В Clean Architecture и Hexagonal Architecture да и в DDD в целом, основная обязанность сервиса - это выполнение важного бизнес-процесса, преобразование объекта предметной области путем изменения его состава.
Как провести грань между сервисом и сущностью, кто из них должен реализовавывать ту или иную операцию? Во-первых, все бизнес-правила, которые являются общими для всей системы должны быть выражены в операциях Сущности. Во-вторых, нужно задать себе вопрос, явлется ли данная операция основной для бизнес-сценария (как например, привязка оказываемых услуг к сущности Эксперта) или вспомогательной (отправка уведомления, валидация входящих данных, сохранение/загрузка), в первом случае это обязанность Сущности, во втором сервиса или службы. В-третьих, все операции, которые не относятся к естественным обязанностям объекта Сущности должны быть вынесены в сервис (службу).
Также в некоторых случаях - например операция сбора и чтения данных для предтавления пользователю (в нашем случае интерфейсы *Query) создание и использование дополнительной Сущности избыточно и не нужно и только затруднит понимание кода. В таких случаях допускается использование проекций данных и анемичных моделей - что то вроде View.
Опишем сценарий создания Эксперта:
- Осуществляем проверку входных данных, на заполненность всех необходимых полей, если какие-либо поля не заполнены, бросаем исключение.
- Создаем Эксперта, используя входные данные.
- Проверяем что Услуги, которые указаны при создании Эксперта сущестуют в нашей системе и, если они существуют, приписываем Эксперту оказываемые Услуги.
- Сохраняем нового Эксперта.
Теперь реализуем это в коде:
На вход приходят данные для создания Эксперта, которые содержат всю необходимую информацию:
Далее нам необходимо проверить, что входящий запрос содержит корректную информацию, для этих целей также используется валидатор YAVI. Только в этом случае, он задается прямо в сервисе, потому что является частью бизнес-логики:
Ну и сам процесс проверки входящего запроса:
Далее, так как информация валидна и достаточна для создания Эксперта. Мы конструируем нужную нам сущность, после создание производим назначение Услуг (если они были заданы) Эксперту:
Последний шаг - это сохранение Эксперта, здесь стоит обратить внимание, на то что агрегат сохраняется полностью, со всеми компонентами, реализация сохранения производится в реализации порта SaveExpertPort (ее обсудим, когда будем добавлять БД к нашему сервису):
Итоговый результат:
Переходим к тестам.
Тестирование Бизнес-логики
Пока наши сервисы не содержат сложной и запутанной бизнес-логики, мы можем ограничиться небольшими не сильно разветвленными юнит тестами. По мере роста сложности бизнес-логики тесткейсы будут размножаться и содержать более сложные ветвления. В текущей ситуации команду создания Эксперта следующими сценариями:
- Положительный сценарий, когда вся необходимая информация задана, но не заданы Услуги.
- Положительный сценарий, когда вся необходимая информация задана и заданы Услуги, которые будет предоставлять Эксперт.
- Отрицательный сценарий, когда в запрос не добавили необходимую информацию - номер телефона.
Полный листинг кода теста:
Настало время перейти к слою данных и веб слою.
Слой данных
Реализуя слой данных, мы можем воспользоваться основным и на мой взгляд главным преимуществом такой архитектуры - это независимость деталей реализации взаимодействия с внешним миром от бизнес-логики. Более того, как и предлагает Роберт Мартин (Ancle Bob), мы вначале разработки приложения не будем погружаться в детали реализации хранилища наших сущностей и других компонентов, а воспользуемся сохранением всех элементов в память приложения или файл на диске. А после того, как наше приложение созреет, мы уже выберем реализацию хранилища. Это может быть как реляционная база данных, так и колоночная база данных или же NoSQL объектное хранилище типа Mongo. На более поздних этапах разработки нам с более высокой вероятностью будет понятно то, каким требованиям должна удовлетворять структура хранилища, в этот момент мы и выберем необходимую технологию и структуру хранилища. Это позволит нам более эффективно использовать все преимущества той или иной технологии.
Подключаем библиотеки
Как и описывалось выше, реализуем хранилище в виде файла на диске, просто для того, чтобы информация никуда не пропадала при перезапуске приложения. А поможет нам в этом библиотека - mapdb.
Добавим соответствующую зависимость в build.gradle:
Теперь реализуем в адаптере порты SaveExpertPort, GetAllExpertsPort. В данный момент не будем разбивать реализацию для каждого порта, а имплементируем ее в одном адаптере. Все сущности Экспертов со всеми значениями будут храниться в одном файле. Пока не будем хранить отдельно сущности свзязи экспертов с Услугами. Итоговая реализация будет выглядеть так:
Как и обсуждалось выше, на данном этапе нам достаточно такой реализации. Позже, когда мы финализируем до определенной степени наши сущности и бизнес-логику, тогда уже добавим необходимое хранилище, соответствующее нашим требованиям
Добавляем веб
Настало время для выставления во вне API эндпоинтов. Реализуем простой rest контроллер, который будет вызывать наши команды и отдавать результаты их работы. Код контроллера:
Не забываем конфигурацию
Теперь, чтобы все заработало, нам необходимо определить необходимые spring бины и сконфигурировать их. Конечно это можно сделать через аннотации (@Component, @Repository и другие), но в данном примере мы будем использовать явное определение бинов и их конфигурирование. Конфигурация, необходимая для создания Эксперта:
Проверка сервиса
Запустим и проверим наш сервис. Стартуем spring-boot приложение:
Теперь воспользуемся rest client и попробуем создать Эксперта. Сначала получим список доступных Услуг:
В ответ получим, введенные нами услуги:
Потом создадим эксперта с определенной Услугой:
Результат работы метода:
И получим список всех экспертов, убедившись, что эксперт добавлен:
Заключение
Надеюсь в этой части мне удалось показать основные преимущества гексагональной архитектуры - тестируемость бизнес-правил, что позволит делать более качественные приложения, и независимость от деталей реализации, что на первых порах разработки системы позволит не задумываться о проектировании некоторых аспектов системы, которые могут быть разработаны уже постфактум, когда нам это понадобиться и мы уже будем уверены в выборе той или иной технологии. В следующей части, попробуем добавить к нашему приложению API First подход и также раскроем один из недостатков гексагональной архитектуры - конвертация запросов и ответов между слоями. Код текущей части доступен в github https://github.com/kazakovav/hex-architecture/tree/3_Add_first_functionality/workspace/schedule
Спасибо за внимание!
Подпишись на мой канал в telegram