Найти тему

Гексагональная архитектура, и как я к ней пришёл

Оглавление

Рано или поздно практически любой разработчик программного обеспечения приходит к поиску архитектурных решений, которые бы облегчали ему процесс разработки и развития проектов. В первую очередь это касается различных шаблонов проектирования, вроде фабрик, посетителей, синглтонов и многих других. Но в какой-то момент становится понятно, что их недостаточно, и нужны какие-то крупномасштабные решения, позволяющие организовать архитектуру всего проекта. В большинстве случаев выбор падает на какую-нибудь многоуровневую архитектуру, одной из главных целей которых является инверсия зависимостей.

Достаточно продолжительное время я в своих проектах использовал обычную многоуровневую архитектуру, где логика приложения находится в компоненте, получающем вызовы извне (REST-эндпоинт, SOAP-сервис, веб-контроллер, слушатель очередей сообщений и т.д.), бизнес-логика в сервисе бизнес-логики, а работа с СУБД в классе-репозитории.

И какое-то время меня такая архитектура полностью устраивала, а выгода от гексагональной архитектуры или от чистой архитектуры, описанной Робертом Мартином в его одноимённой книге, мне оставалась непонятной. Основной смысл обоих архитектурных решений заключается в максимальном применении SOLID и атомизации компонентов исходного кода. Если вкратце, то проект должен содержать только функциональные интерфейсы: интерфейсы управляющих (ведущих/входящих, driving/input) портов в качестве интерфейсов бизнес-логики и интерфейсы управляемых (ведомых/исходящих, driven/output) портов в качестве инфраструктурных интерфейсов, например, репозиториев.

Но в какой-то момент используемая мной многоуровневая архитектура перестала быть удобной даже в средних проектах. Основные проблемы, с которыми мне пришлось столкнуться:

  • Большой объём кода в рамках одного класса. В первую очередь это касается компонентов бизнес-логики. С такими классами очень сложно работать без помощи инструментов IDE. С кодом тестовых классов работать ещё сложнее, поскольку тестового когда в 3-5 раз больше тестируемого. Рефакторинг компонентов бизнес-логики может быть достаточно ресурсоёмкой затеей.
  • Негибкость кода, проявляющаяся, например, в невозможности использовать разные реализации одного компонента в разных ситуациях в рамках одного сервиса. Например, мы можем использовать кешированные данные в операциях чтения, но это недопустимо в операциях записи.
  • Отсутствие возможности масштабировать только необходимые компоненты, ровно, как и отсутствие возможности разделения на компоненты чтения и записи.

Постоянные конфликты при слиянии изменений из нескольких веток системы контроля версий, если речь идёт о работе в команде.

Да, соглашусь с мнением, что ничто не мешает разделять методы между несколькими интерфейсами по какому-нибудь принципу. Но этот самый принцип будет всегда строго индивидуален для каждого проекта, и его невозможно будет сформировать на 100% заранее.

В итоге, столкнувшись с вышеописанными проблемами, я решил перечитать книгу Роберта Мартина, а заодно почитать статьи на тему гексагональной архитектуры.

Пример типичной для меня многоуровневой архитектуры
Пример типичной для меня многоуровневой архитектуры

Общие положения и терминология

Как и у других многоуровневых архитектур, главной целью у гексагональной архитектуры является отделение компонентов, входящими в ядро программы от компонентов, к нему не относящимся. К ядру программы, например, не относятся контроллеры, репозитории или презентаторы. Все они в гексагональной архитектуре относятся к инфраструктурным компонентам.

Ядро проекта с гексагональной архитектурой представлено как минимум тремя видами компонентов:

  • Управляющими портами, при помощи которых объявляются открытые API ядра.
  • Адаптерами управляющих портов, которые реализуют логику ядра проекта.
  • Управляемыми портами, при помощи которых ядро может обращаться к внешним компонентам: базам данных, очередям сообщений, почтовым серверам и т.д.

Компоненты, обращающиеся к ядру при помощи управляющих портов, называются драйверами. Например, таковыми могут быть REST-эндпоинты или SOAP-сервисы. Компоненты, реализующие управляемые порты тоже называются адаптерами, но они, как и драйверы не являются частью ядра, так как ядро не интересуют такие подробности, например, как в какой базе данных хранятся данные, какая библиотека используется для работы с очередями сообщений, используется ли кеширование и так далее.

Впрочем, кроме адаптеров управляющих портов логику ядра могут содержать и другие, входящие в ядро, компоненты. Например, таковыми могут быть классы-агрегаты или предметные сервисы, если речь идёт о проектах, разрабатываемых с применением практик предметно-ориентированного проектирования.

SOLID как основа гексагональной архитектуры

Принципы SOLID, которые я уже рассматривал в своих постах, можно трактовать в отношении архитектуры проекта следующим образом:

  • Принцип единственной ответственности (SRP) гласит, что отдельный компонент должен отвечать за одно действие.
  • Принцип открытости/закрытости (OCP), гласит, что код должен быть открыт к расширению, но закрыт к изменению. Иными словами, если класс должен быть расширен, то он должен быть абстрактным, если нет, то финальным.
  • Принцип подстановки [Барбары] Лисков (LSP) гласит, что компоненты, реализующие один и тот же интерфейс, должны быть взаимозаменяемыми.
  • Принцип разделения интерфейсов (ISP) гласит, что вместо одного общего интерфейса должно использоваться множество специализированных.
  • Принцип инверсии зависимостей (DIP) гласит, что компоненты должны зависеть от абстракций, а не конкретных классов.

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

Вариант, который приглянулся мне больше всего — придерживаться той же логики, один класс — реализация одного интерфейса. Каждый такой класс будет содержать свой минимум зависимостей и кода, ровно, как и его тестовый класс. Кроме того такая атомизация кода позволяет из одной кодовой базы получить абсолютно разные сервисы:

  • Можно собрать модульный монолит, который предоставляет полный набор API проекта.
  • Можно собрать набор микросервисов, каждый из которых предоставляет API своей предметной области, если их в проекте несколько.
  • Можно собрать отдельные микросевисы для API запросов и API команд, применив архитектурный шаблон проектирования CQRS.
  • В конечном итоге можно собрать для каждого метода API отдельный микросервис, получив в итоге FaaS.

Таким образом гексагональная архитектура может быть идеальной основой для проектов с микросервисной архитектурой.

Альтернативный вариант — реализовывать несколько взаимосвязанных интерфейсов в одном классе, но помнить, что при необходимости этот класс можно разделить.

Применение, плюсы и минусы

Стоит отметить, что применять гексагональную архитектуру или родственную ей чистую архитектуру Роберта Мартина можно в проектах любого масштаба, но с одной оговоркой: в маленьких проектах и стартапах не всегда есть смысл стремиться сразу к максимальной атомизации кода, описанной выше. Скорее всего, некоторое время будет удобнее работать с классами, реализующими сразу несколько интерфейсов, либо зависящими сразу от нескольких интерфейсов.

Какие плюсы я для себя отметил в применении гексагональной архитектуры:

  • Работать с кодом классов и тестов становится значительно проще. Проще найти, проще прочитать, проще изменить. Плюс IDE больше не напрягается при рефакторинге.
  • Максимальная гибкость конфигурации сервисов. Если я хочу добавить кеширование для одного метода, то я добавляю кеширование только для него. Если мне нужно масштабировать только определённые API, то я масштабирую только их.
  • Меньше конфликтов при слиянии веток в системах контроля версий, поскольку каждый разработчик работает с отдельным набором интерфейсов и классов.

Но в любой бочке мёда найдётся ложка дёгтя, так и в случае с гексагональной архитектурой.

  • Большое количество интерфейсов и классов. Был у вас интерфейс объявляющий десяток методов и класс, реализующий его, а теперь у вас десяток интерфейсов, десяток классов и ещё десяток тестовых классов. Эффективный объём кода останется практически неизменным, хотя увеличится количество строк кода, зато в разы увеличится количество типов. Тут единственным способом борьбы будет грамотная организация типов по пакетам и модулям.
  • Имена типов и методов. Эта проблема, скорее связана не с самой архитектурой, а с рекомендуемыми подходами к именованию компонентов, которые описаны и в книгах Роберта Мартина и Стивена МакКоннелла, но при использовании гексагональной архитектуры вам постоянно придётся сталкиваться с очень длинными именами типов и методов.
Пример применения гексагональной архитектуры
Пример применения гексагональной архитектуры

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

Оригинальная статья у меня на сайте.