Hexagonal Architecture / 3.1. Добавляем API списка и создания мастера
Что будем делать
Разобравшись с тем, как создавать многомодульное приложение при помощи gradle, пора приступить к добавлению фукнциональности в скелет приложения. Первым делом, добавим возможность получать список мастеров и список услуг, которые они предоставляют. Реализовывать функционал будем в следующей последовательности - от внутренних слоев к внешним. Таким образом мы в первую очередь сосредоточимся на бизнес-логике. Сначала создадим доменную модель, потом бизнес-логику (сервисы) и уже потом реализуем адаптеры.
Доменная модель
Моделируем объекты
В текущей реализации, так как пример является обучающим/показательным, мы не будем много времени посвящать бизнес-моделированию доменной области и ограничимся лишь поверхностным анализом. Однако, для того чтобы хорошо спроектировать и описать доменную модель и правильно разделить ее на сущности, агрегаты и значения, необходимо первоначально произвести глубокий анализ и моделирование процессов. Это позволит уже на первом этапе создать модели и правильно описать их основные свойства, а соответственно и написать тесты для этих сценариев, что позволит значительно уменьшить число ошибок в конечном продукте. Для бизнес-моделирования придумано много методов и методик, например Event Storming (https://www.eventstorming.com)
Дальнейшее моделирование будет основано на моем оторванном от реальности предположении/понимании бизнес-модели работы салонов красоты (🙂).
Нужно полагать, что проектирование моделей, особенно на ранней стадии разработки и в условиях быстрого изменения требований к системе, никогда с первого раза не будет финальным. Поэтому в процессе развития системы модели могут дополняться, изменяться, разбиваться на несколько, собираться из нескольких и т.д. Поэтому не стоит относиться к ним как к постулату. Если модель требует изменения, то она должна измениться.
В походе Domain Driven Design сначала предлагается выбрать и отделить Смысловое Ядро системы - это то, что приносит бизнесу деньги. Похоже, что в нашем случае смысловым ядром будет оказание Экспертом Услуг Клиенту. Но в рамках этой части мы возьмем вспомогательную сущность Эксперт, а работу со сложным смысловым ядром опишем уже после, когда разберемся во всех нюансах реализации гексагональной архитектуры.
Один из основных участников нашей системы - это мастер (Эксперт) предоставляющий определенные Услуги Клиентам. На текущем уровне нашего понимания системы, мы можем выделить 3 сущности в нашей системе - Эксперт, Услуга, Клиент. Сосредоточимся пока на первых двух. Какими свойствами они должны обладать:
Эксперт:
- Идентификатор эксперта (ведь мы должны отличать экспертов в рамках системы).
- Персональная информация - ФИО, дата рождения.
- Контактная информация - телефон, e-mail.
- Налоговая информация - инн, снилс.
Услуга:
- идентификатор услуги.
- наименование услуги.
Выразим модели в коде
Услуга:
Услуга состоит пока только из 2-х полей - id и name. Здесь идентификатор задается отдельным классом:
Это сделано, для того, чтобы явно отделить идентификаторы конкретных сущностей от идентификаторов объектов-значений и остальных вспомогательных объектов.
Помимо основных аннотаций lombok здесь пристствует аннотация @Jacksonized - эта аннотация вспомогательная, которая используется совместно с @Builder и @SuperBuilder, она автоматически конфигурирует builder для десериализации с помощью Jackson.
Теперь перейдем к сущности Эксперта, пока очевидно, что некоторые свойства можно объединиться в отдельные классы значения, а Эксперт будет агрегатом объединяющим их. Исходя из этих соображений разделим свойства на 3 класса:
PersonalInfo:
ContactInfo:
и TaxInfo:
Соответственно наша сущность Эксперт будет выглядеть так:
Сейчас сущность содержит только поля с информацией и является так называемой анемичной моделью - то есть той, которая несет в себе только данные, но не действия и правила. В таких случаях реализация всех правил возлагается на сервисный слой. В нашем же случае, сущность будет содержать бизнес-правила, которые свойственны ей.
Добавляем бизнес-правила
Первым делом, добавим методы создания новой сущности, они должны позволить создать Эксперта с указанием хотя бы персональной и контактной информации и присвоить ему уникальный идентификатор:
Здесь мы воспользуемся builder’ом, а не конструктором по умолчанию по следующим причинам - необходимо проверить, что для создания Эксперта заполнили обязательные поля, если какое-либо поле не заполнено, то должно возникнуть исключение и не позволить создать сущность с недостаточными данными. Данная логика могла бы быть вынесена в отдельный метод и вызываться каждый раз, когда нам нужно, но чтобы избежать дублирования данного кода, вынесем его на этап построения объекта(builder), в этом случае мы еще и не позволим десериализовать “некорректный” объект. Для этих целей переопределим builder, который создает lombok:
Прежде чем возвращать созданый объект, мы вызовем некоторый валидатор, который проверит поля объекта по определенным правилам и, если нарушений не обнаружит, то вернет вновь созданный объект, либо бросит исключение. В качестве валидатора мы можем использовать механизм валидации jakarta.validation (javax.validation) или написать свой. В последнее время мне нравится использовать YAVI (https://yavi.ik.am/) - он декларативный, функциональный и, на мой взгляд, более гибкий, по сравнению с механизмом аннотаций. Текущий валидатор:
Валидатор помещен в объект сущности для наглядности, так видно все правила, которым подчиняется сущность. Но если правила валидации вырастут до больших размеров, то стоит выделить отдельный класс для валидатора.
Результат
Также в объект сущности Эксперт добавим метод, который позволяет Эксперту “присвоить” выполняемые услуги. Пока этот метод будет максимально простым. Итоговый класс будет выглядеть так:
В валидаторе также используются некоторые объекты валидаторов из объектов значений входящих в состав агрегата (ContactInfo.requirePhoneValidator и TaxInfo.validator). С точки зрения OOD это и правильно и неправильно, с одной стороны, исходя из нашей логики, каждый объект должен сам знать, как его надо проверять, но объекты-значения могут быть использованы в нескольких агрегатах, а в этом случае у каждого агрегата или сущности будут свои правила валидации этих объектов. В данном случае предлагается держать несколько объектов валидаторов в классах объектов-значений для удобства их группировки (или вынести в отдельный) и использовать тот, который актуален в данный момент.
В результате всех действий с кодом мы получаем следующую структуру:
Тестирование
Большим плюсом гексагональной архитектуры является ее тестируемость. Если код организован правильно, то будет не очень сложно написать юнит тесты. Добавим юнит тесты для нескольких сценариев создания Эксперта:
Настало время перейти на более верхний слой архитектуры - слой пользовательских сценариев. Как обычно код доступен в гилабе https://github.com/kazakovav/hex-architecture/tree/3_Add_first_functionality/workspace/schedule
Во второй части разберем сервисный слой и слои данных и веб, а так же соберем все вместе.
Спасибо за внимание!
Подпишись на мой telegram-канал