В 2017 году Matthias Noback (автор A year with Symfony) опубликовал цикл из трех статей, в котором описал свои взгляды на идеальную архитектру корпоративных приложений, сформировавшуюся за долгие годы практики.Первая часть является вводной и не представляет особого интереса(можно ознакомитсья в оригинале). Переводом второй является данная статья. Перевод третьей будет доступен в скором времени.
Для меня, одним из обязательных требований, к "чистой" архитектуре, является грамотное разделение кода приложения по слоям. Сам слой не делает ничего, вся соль в том, как он используется и какие ограничения накладываются на компоненты, ему принадлежащие. Давайте немного пофилософствуем, перед тем как рассмотреть конеретные практические приемы.
Зачем нужны слои
- Слои помогают спрятать/защитить, то, что находится под ними. Можно воспринимать слой как фильтрующий барьер: данные, передающиеся через него, должны быть провалидированны перед тем как перейти на следующий. Они должны быть приведены к формату, который позволит другим слоям корректно работать с ними. Слой также определяет, какие данные и функции из более глубокого слоя могут быть использованы во внешних.
- Слои четко разграничивают ответственности, а следовательно расположение классов в вашем коде. Если вы добьетесь строгих договоренностей внтури вашей команды, о том какие слои используются в вашем приложении и за что отвечате каждый из них, то вам всегда будет легко найти нужный класс или определится куда следует добавлять новый, просто зная его предназначение.
- Благодаря использованию слоев, можно свободно менять приоритет и порядок этапов разработки приложения. Вы можете разрабатывать проект последовательно, начиная от ядра бизнесс логики, накладывая слой за слоем на него. А можно инверсировать процесс и начать с разработки слоя взаимодействия с пользователем. Этот пункт довольно важен для нас, так как благодаря ему можно разработать большую часть приложения до принятия решения о используемой ORM, БД, фреймворке, и т.д
- Большое количество старого софта содержит код, неразделенный на слои, который можно назвать "спагети" кодом: вы можете вызывать и использовать всё что хотите, любые методы и структуры в любой части проекта. Используя систему слоев(правильным образом) можно добиться высокого уровня разделения ответсвенности( separation of concerns). Если вы задокументируете эти правила и будете следить за их соблюдением на код ревью, то вы здорово уменьшите скорость скатывания вашего проекта в ранг
гавнокода"технического долга" Вы, конечно же, пишите тесты. Грамотная спроектирвоанная система слоёв, невероятно упрощает тестирование. Различные типы тестов подходят для кода из разных слоев. Назначения каждого теста становится более очевидным. Набор тестов в целом становится более стабильным и более быстроработающим.
Однако, у нас есть паникёр из твиттера:
ООП версия спагетти кода — это код лазанья, с переизбытком слоев.
Лично я никогда не встречал код-лазанью, зато видел очень много лапшекода. Правда бывало, что я писал код, в котором допускал серьезные архитектурные ошибки, и неверно разделял приложение на слои, что приносило некоторые проблемы. В этой статье я описываю, как мне кажется, наилучший набор слоев, большая часть из которых описана в книге Vaughn Vernon "Implementing Domain-Driven Design"(ссылка ниже). Прошу заметить, что слои не имеют жесткой привязки к DDD, хотя они и дают возможность создавать чистые доменные модели, при соответсвующем желании у разработчика.
Структура директорий и неймспейсов
Внутри src/
у меня есть директории для каждого контекста(Bounded Context), например который я выделяю в своем приложении. Каждая из них также служит корневым неймспейсом для принадлежащих ей классов.
Внутри каждого контекста я создаю директории для каждого из слоёв:
- Domain
- Application
- Infrastructure
src/
{BoundedContext}/
Domain/
Model/
Application/
Infrastructure/
Кратко опишу каждый из них.
Слой 1 — Домен(модель/ядро)
Доменный слой содержит классы для известных DDD типов/паттернов:
- Entities
- Value objects
- Domain events
- Repositories
- Domain services
- Factories
- ...
Внутри папки Domain я создаю подпапку Model, внутри неё — директории для каждого из агрегата(Aggregate root). Папка с агрегатом содержит все связанные с ним штуки(объекты-значения, доменные события, интерфейсы репозиториев и т.д)
Обратите внимание, что код из доменного слоя никак не соприкасается с реальным миром. И если бы не тесты, то никто не мог бы обращаться к его объектам напрмяую(это делается через верхние слои). Тесты для доменной модели должны быть исключительно модульными. Т.к доменный слой не взаимодетсвует напрямую с файловой системой, сетью, бд и т.д, то мы получаем стабильные, независимые, чистые и быстрые тесты.
Слой 2 — (обёртка для домена): Прикладной слой
Прикладной слой(Application Layer) содержит классы команд и их обработчиков. Команда представляет собой указание на что-то, что должно быть выполненно.Это обычный DTO(Data Transfer Object), содержащий только примитивные значения. Всегда должен быть обработчик команды, который знает, как нужно выполнить конкретную команду. Обычно обработчик команды (также его называют application service) ответственен за все необходимые взаимодействия — использует данные из команды для создания(или извлечения из базы) агрегата, выполняет над ним какие то операции, может сохранить агрегат после этого.
Код этого слоя также можно покрыть юнит тестами, однако на этом этапе можно начинать писать и приёмочные. Вот хорошая статья на эту тему Modelling by Example от Константина Кудряшова.
Слой 3(обертка для прикладного) — Инфраструктура
Код, написанный в предыдущем слое, тоже не вызываается никем кроме тестов. И только после добавления инфраструктурного слоя, приложение становится рельно пригодным к использованию.
Инфраструктурный слой содержит код, необходимый для взаимодействия приложения с реальным миром — пользователями и внешними сервисами. Например, слой может содержать код для:
- Работы с HTTP
- Общением с БД
- Отправкой емэйлов
- Отправку пушей
- Получением времени
- Генерации случайных чисел
- И т.д и т.п
Код этого слоя надо покрывать интеграционными тестами(в терминологии Freeman and Pryce). Здесь вы тестируете всё по настоящему — настоящая база, настоящий вендорский код, настоящие внешние сервисы. Это возволяет убедиться в работоспособности тех вещей, которые не находятся под вашим контролем но используюся в вашем приложении.
Фреймворки и библиотеки
Все фреймворки и бибилотеки взаимодействующие с внешнем миром(файловой системой, сетью или базой) должны вызываться в инфраструктурном слое. Конечно, код домена и прикладного слоя часто нуждается в функциональности ORM, HTTP клиента и т.д. Но он должен использовать её только через более абстрактные зависимости. Как того и требует правило зависимостей.
Правило зависимостей
Правило зависимостей(сформулированное Robert C. Martin в The Clean Architecture) утвержадет, что на каждом слое приложения вы должны зависеть только от кода текущего или более глубокого слоя. Это значит, что код домена зависит только от себя, код слоя приложения от своего кода или домена, а код инфраструктурного слоя может зависеть от всего. Следуя этому правилу, нельзя сделать в доменном слое зависимость на код из инфрастуруктурного.
Но слепо следовать какому-либо правилу, непонимая в чем его истинный смысл — это довольно глупая затея. Так почему же вы должны использовать правило зависимостей? Следуя этому правилу вы гарантируете, что чистый код слоёв прикладного и доменного слоев не будет завязан на "грязный", нестабильный и непредсказуемый код инфраструктуры. Также, применяя правило зависимостей, вы можете заменить что угодно в инфраструктурном слое не прикасаясь и не изменяя код более губоких слоёв, что даёт нам богатые возможности для ротации и переносимости компонентов.
Этот способ уменьшения связанности модулей известен давно, как Dependency Inversion Principle — буква "D" в SOLID сформулированном Робертом Мартиным: "Код должен зависеть от абстракций, не от реализаций". Практическая реализация в большинстве ооп языков заключается в выделинии публичного интерфейса для всех вещей, от которых вы можете зависеть(интерфейс и будет абстракцией) и создании класса, реализующего этот интерфейс. Этот класс будет содержать детали, не имеющие значения для интерфейса, следовательно этот класс и будет реализацией, о которой говориться в inversion principle.
Архитектура: отсрочка технологических решений
Применяя предложенный набор слоёв вместе с правилом зависимостей, можно получить много плюшек при разработке:
- Можно много эксперементировать, прежде чем принимать такие важные решения, как, к примеру «используемая СУБД». Также можно спокойно использовать разные базы данных для разных случаев в рамках работы с одной и той же моделью.
- Можно отложить решение об используемом фреймворке. Это не позволит стать «приложением Symfony» или «Laravel проектом» в самом начале разработки.
- Фреймворки и библиотеки будут размещены на безопасном расстоянии от кода модели. Это здорово поможет при обновлении мажорных версий этих фреймворков и библиотек. Это также позволит минимизирвоать изменения в коде и трудозатраты, если вы когда-нибудь захотите использовать, к примеру, Symfony 3 вместо Zend Framework 1.
Все это выглядит крайне заманчиво: мне нравится возможность беспроблемной замены компонентов приложения + я люблю принимать важные архитектруные решения не перед стартом проекта(основываясь на своем прошлом опыте и догадках), а тогда, когда начинают проясняться реальные кейсы использования разных частей приложения, и я имею возможность выбирать подходящие решения исходя из существующих потребностей.
Заключение
Как упомяналось ранее, этот вариант расслоения приложения, хорошо уживается с любым фреймворком, т.к его место четко определено в инфраструктурном слое.
Некоторое считают, что в моём варианте "слишком много слоев". Я не понимаю, как можно считать 3 слоя, слишком большим количеством, но если вас это смущает то можете убрать прикладной. Вы потеряете возможность писать приемочные тесты(они станут чем то похожи на системные — более медленные и хрупкие) и не сможете тестировать один и тот же функционал вызываемый к примеру из веб-интерфейса и консольной команды без дублирования кода. В любом случае, вы сильно улучшите архитектуру вашего проекта благодаря раделению бизнесс логики и инфраструктурной части.
Осталось более подробно рассмотреть инфраструктурный слой. Так мы плавно перейдем к теме гексагональной архитектуры(порты и адаптеры). Но всё это, в следующей части.
Дальнейшее чтение
- Growing Object-Oriented Software Guided by Tests by Steve Freeman and Nat Pryce
- Screaming Architecture by Robert C. Martin
- The Clean Architecture by Robert C. Martin
- Implementing Domain-Driven Design, chapter 4: "Architecture" and chapter 9: "Modules", by Vaughn Vernon
Также можно ознакомиться с Deptrac — инструмент, помогающий соблюдать правила использования слоев и зависиомостей.