На третий день отпуска в деревне мозг потребовал активных развлечений. Гибсоновская «Машина различий» благополучно переварилась и очередным кандидатом на расправу был выбран порядком подзабытый блог, в черновиках которого зависло уже приличное количество тем на проработку.
Отпуск — пора легкого времяпровождения, поэтому и тему для препарирования я выберу полегче, чтобы кода поменьше, а картинок побольше. Вот и поговорим мы в очередной раз про распределение обязанностей и управление зависимостями, но на сей раз на уровне модулей приложения.
Архитектура по-умолчанию
Итак, мы начинаем новый проект. Чтобы легче было работать, пусть это будет какой-нибудь интернет-магазин со вполне себе ординарной функциональностью: какая-нибудь витрина, корзина и система заказов. Открыв любимую IDE, через несколько пассов у нас получится приложение с примерно такой структурой:
Стрелочками обозначены ссылки между проектами.
Наверняка, сначала будет разработана база данных. Затем через какой-нибудь DAL сущности из проекта BusinessLogic будут этими самыми данными манипулировать. Ну и UI-проект все это будет отображать, собирать какой-то пользовательский ввод и дергать за ниточки бизнес-логику.
Все бы хорошо, но данная схема подразумевает первичность базы данных. Ее разработка влияет на все остальные уровни. И в случае, если мы хотим смоделировать бизнес-логику, используя Domain Driven Design, это влияние будет столь значительным, что рано или поздно все попытки разработать полноценную модель скатятся в использование Active Record или Table Module, похоронив все дальнейшие попытки использования преимуществ возможностей богатой модели.
Кроме того, сильная связь с базой данных значительным образом усложняет тестирование: при всех возможностях современных фреймворков, тестирование такого кода всегда остается одним большим компромиссом.
А что если бы появилась возможность сконцентрироваться на разработке модели домена, как на первоочередном модуле?
Ресурсами наружу
Решение проблемы с первоочередностью разработки модели домена (как, в общем случае, и любой другой логики, связанной с разрабатываемым приложением) была хорошо сформулирована Алистером Кокберном в виде паттерна Hexagonal Architecture.
Вкратце, идея заключается в том, что все внешние процессы и ресурсы по отношению к разрабатываемой системе должны подключаться через специальные порты и адаптеры. К этим ресурсам относится файловая система, сетевые хранилища, всевозможные API третьих систем и, что имеет ключевое значение — база данных и пользовательский интерфейс.
Перерисовав структуру нашего приложения согласно вышеописанному паттерну, мы получим примерно следующее:
Таким образом срабатывает принцип обращения зависимостей, когда код различных уровней связываются исключительно через абстракции, не используя никаких деталей реализации: бизнес-логика знает лишь, что какие-то сущности куда-то сохраняются, работая с высокоуровневым репозиторием, а так же, что реакцию на пользовательский ввод нужно осуществлять через такие же высокоуровневые контроллеры.
Кажется, мы ответили на вопрос о том, как развернуть зависимости так, чтобы приоритет разработки сместился в сторону бизнес-логики с логики сохранения данных. Но как быть с логикой уровня приложения? Что делать с наддоменными сервисами, например, с сервисами управления доступом? С одной стороны, мы можем подключить такой сервис через пару порт-адаптер, но с другой стороны, мы таким образом не решим вопрос изоляции логики уровня домена от логики уровня приложения.
Центр управляет всем
В качестве решения этой проблемы предлагается использование так называемой «луковой архитектуры» (onion architecture). У этого паттерна такая же мотивация, что и у гексагональной архитектуры, но решение строится не просто на отделении абстракций от реализации, а на управлении доступом между различными уровнями архитектуры.
Если мы перерисуем структуру нашего проекта, согласно предложенной схеме, то у нас получится такой рисунок:
Смысл данного подхода заключается в следующем:
- В ядре системы находится модуль, не связанный ни с чем. Чистая абстракция предметной области;
- Другие модули выстраиваются вокруг ядра таким образом, чтобы их зависимости были направлены вовнутрь к ядру;
- Зависимости в противоположную торону от ядра недопустимы;
- Оболочка системы состоит из модулей конкретных реализаций внутренних асбтракций.
Таким образом мы видим, что предлагаемый паттерн работает точно так же, как и гексагональная архитектура, но дополнение ко всему устанавливает правила игры для внутренней структуры многогранного ядра.
Заключение
Трехзвенная архитектура не давала нам ответы на вопросы о том, как нам проектировать приложение, опираясь в первую очередь на модель домена, изолировав ее от инфраструктуры и логики уровня приложения. В то время, как вооружившись принципом обращения зависимостей мы увидели, как можно строить приложения вокруг какого угодно центра.
Но важно отметить, что рассмотренные паттерны помогают нам строить так называемую горизонтальную структуру приложения, в то время как на прикладном уровне существует своя техника определения границ контекстов и их изоляцией.
Кроме того, не стоит забывать об использовании всевозможных Dependency Injection-фреймворкам, которые помогут избежать трудностей при управлении временем жизни объектов в такой вот «вывернутой» архитектуре.
Отпуск продолжается :)