Matthias Noback (автор A year with Symfony) опубликовал цикл из трех статей, в котором описал свои взгляды на идеальную архитектру корпоративных приложений, сформировавшуюся за долгие годы практики.Первая часть является вводной и не представляет особого интереса(можно ознакомиться в оригинале). Перевод второй — по ссылке. И так как он вызвал БЕШЕННЫЙ ажиотаж(целых ДВА человека подискутировали со мной в комментах), то не перевести третью было бы преступлением.


В предыдущей статье мы обсудили разумную систему расслоения проекта, состоящую из трех слоёв:


  • Домен
  • Прикладной слой
  • Инфраструктура

Сейчас, подробно рассмотрим инфраструктурный слой.


Инфраструктура


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


  • Файловой системой
  • Сетью
  • Пользователями
  • Орм
  • Фреймворком
  • Сторонними либами
  • ...

Порты


Слоистая система сама по себе уже неплохо разделяет ответственности. Однако, можно пойти дальше и подробно рассмотреть все точки, через которые наше приложение общается с внешним миром. Alistair Cockburn называет их "портами" в своей статье Hexagonal architecture. Порт — абстрактное понятие, он может не иметь никакого представления в коде(кроме неймспейса/директории, как я покажу ниже). Может называться как-то вроде:


  • UserInterface
  • API
  • TestRunner
  • Persistence
  • Notifications

Другими словами, для каждого места, через которое ваше приложение может принимать запросы(через веб-интерфейс, API, и т.д) существует порт, ровно также как и для каждого способа которым приложение может выводить данные "наружу"(сохранение на диск или в базу, отправка данных по сети, рассылка уведомлений пушами или по почте и т.д). Я часто использую термины порты ввода и вывода( input and output).


Что считать или не считать портом — дело вкуса. В крайнем случае можно выделять порт под абсолютно каждый юзкейс, делая сотни портов для разных приложений

— Alistair Cockburn

Адаперы


Для каждого из абстрактных портов нужен код, который будет делать "реальную" работу. Нам нужен код для непосредственной обработки HTTP сообщений, который позволял бы нашему приложению общатсья с пользователем через web. Нужен код для взаимодействия с БД(возможно "говорящей" на SQL), чтобы сохранять и получать наши данные. Код, написанный для работы с портами называется "адаптером". Мы всегда пишем хотя бы по одному адаптеру для каждого из портов нашего приложения.


Адаптеры максимально конкретны. Они содержат реальный низкоуровневый код, чем и отличаются от портов(которые существуют как абстракция). Т.к как код адаптера — это код, связывающий приложение с реальным миром, то адаптер — это часть инфраструктурного кода, и распологается он на соответствующем слое. Тут "порты и адаптеры" здорово сочетаются со слоистой архитектурой.


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


Если вы будете применять этот принцип для всех портов, то вы сможете легко писать альтернативные адаптеры для них и "жонглировать" ими в процессе разработки. Вы сможете запустить и поэкспеременитровать с адаптером Mongo бок о бок с адаптером MySql. Сможете ускорить тесты прикладного уровня подменяя реальный адаптер более быстрой заглушкой(например фейковый адаптер, который не делает обращение к реальной файловой системе или сети, а просто хранит нужную инфу в памяти).


Структура директорий


Зная какие порты и адаптеры есть или будут в вашем приложении, я бы рекомендовал отразить их в следущей структуре неймспейсов/директорий:


src/
    <BoundedContext>/
        Domain/
            Model/
        Application/
        Infrastructure/
            <Port>/
                <Adapter>/
                <Adapter>/
                ...
            <Port>/
                <Adapter>/
                <Adapter>/
                ...
            ...
    <BoundedContext>/
        ...

Интеграция Bounded Contexts


Специально для фанатов DDD — при интеграции Bounded Contexts я понял что имеет смысл выделять порты для каждого взаимодействия между контекстами. Вы можете прочитать хороший пример с REST API в 13 главе "Integrating Bounded Contexts" книги "Implementing Domain-Driven Design" от Vaughn Vernon.


Вкратце: представим, что есть Identity & Access отвечяющий за идентификацию и уровни доступа пользователей. А есть CollaborationСontext, определяющий различный типы ролей: авторы, создатели, модеры и т.д. Для того чтобы не нарушать консистентность, CollaborationСontext должен всегда запрашивать у Identity & Access действительно ли существует конкретный пользователь и имеет ли он достаточно прав для той или иной роли. Чтобы убедиться в этом, CollaborationСontext нужно дернуть REST API Identity & Access по HTTP.


В терминологии портов и адаптеров, взаимодействие между этими контекстами может быть представлено так: порт IdentityAndAccess внутри CollaborationСontext с адаптером для этого порта — например HTTP или любой другой технологии передачи данных. Структура папок/неймспейсов может быть такой:


src/
    IdentityAndAccess/
        Domain/
        Application/
        Infrastructure/
            Api/
                Http/ # Serving a restfull HTTP API
    Collaboration/
        Domain/
        Application/
        Infrastructure/
            IdentityAndAccess/
                Http/ # HTTP client for I & A's REST API

В принципе, можно даже сделать адаптер, который не будет делать сетевых вызовов, но будет тайком лезть прямо в код или БД Identity & Access чтобы получить нужную инфу. В некоторых случаях это может быть разумным решением, если вы четко понимаете, какие риски могут возникнуть при недостаточно жестком разграничении контекстов. В конце концов, контексты помогают избегать "большого кома грязи", в ситуациях, когда границы доменной модели не ясны.


Заключение


Этот пост завершает мой цикл статей "Слои, порты и адаптеры". Надеюсь, знания полученные тут пригодятся вам в следующем проекте, а возможно вы сможете применить(частично) их и в текущем. Буду рад услышать о реальном практическом опыте их использования. Если вам есть что рассказать, то можете сделать это в комментариях к посту.

Комментарии (1)


  1. bm13kk
    04.04.2018 09:17

    За 3 статьи, я так и не понял, что такое application layer