Skip to content

Шестигранная архитектура - Что это такое? Почему вам стоит её использовать?

Перевод статьи: Hexagonal Architecture - What Is It? Why Should You Use It? - Sven Woltmann

Какова цель программной архитектуры?

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

A “historically grown” software architecture Программная архитектура, выросшая исторически

Согласно книге Роберта С. Мартина “Clean Architecture”, хорошая архитектура позволяет изменять программное обеспечение на протяжении его жизненного цикла с минимальными постоянными усилиями (и соответственно предсказуемыми затратами для клиента).

Изменения могут включать:

  • Реализацию запросов клиентов.
  • Адаптацию к изменениям в законодательных требованиях.
  • Использование более современных технологий (например, замену SOAP API на REST API).
  • Обновление компонентов инфраструктуры (например, обновление серверов баз данных или библиотек ORM до новой версии).
  • Замена сторонних систем (например, внешней системы выставления счетов или доставки новостных рассылок).
  • Даже замену сервера приложений (например, использование Quarkus вместо Glassfish).

Как разработать хорошую программную архитектуру?

Чтобы сохранить программное обеспечение "мягким", приложение должно быть разделено на хорошо изолированные, независимо разрабатываемые и тестируемые компоненты. (Автоматизированное развертывание также необходимо, но не является темой этой статьи).

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

Classic three-layer architecture Классическая трехслойная архитектура

Однако практика показала, что слоистая архитектура не подходит для больших проектов. Вы узнаете почему в следующем разделе.

Недостатки слоистой архитектуры

Слоистая архитектура приводит к ненужной связи с негативными последствиями:

Бизнес-логика напрямую зависит от базы данных, в то время как презентационный слой имеет транзитивную зависимость. Например, все сущности, репозитории и библиотеки ORM (такие как Hibernate или EclipseLink в мире Java) также доступны в презентационном слое. Это искушает разработчиков ослабить границы между слоями, особенно когда они под давлением времени.

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

Связь также усложняет обновление базы данных или слоя доступа к данным (например, до новой версии базы данных или новой версии ORM). Я видел множество бизнес-приложений, работающих на устаревших (то есть с ошибками и/или небезопасных) версиях Hibernate или EclipseLink, потому что обновление потребует корректировок во всех слоях приложения и было понижено в приоритете менеджментом.

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

Ослабление границ слоев также делает невозможным тестирование отдельных компонентов в изоляции – например, бизнес-логики без пользовательского интерфейса и базы данных.

Что такое шестигранная архитектура?

Алистер Кокберн представил шестигранную программную архитектуру в блоге в 2005 году. Кокберн ставит следующие цели:

  • Приложение должно быть одинаково управляемо пользователями, другими приложениями или автоматическими тестами. Для бизнес-логики не имеет значения, вызывается ли она из пользовательского интерфейса, REST API или тестовой среды.
  • Бизнес-логика должна быть разработана и тестироваться в изоляции от базы данных, другой инфраструктуры и сторонних систем. С точки зрения бизнес-логики не имеет значения, хранятся ли данные в реляционной базе данных, системе NoSQL, XML файлах или проприетарном бинарном формате.
  • Модернизация инфраструктуры (например, обновление сервера базы данных, адаптация к измененным внешним интерфейсам, обновление небезопасных библиотек) должна быть возможна без корректировок в бизнес-логике.

В следующих разделах вы узнаете, как шестигранная архитектура достигает этих целей.

Порты и адаптеры

Изоляция бизнес-логики (называемой "приложением" в шестигранной архитектуре) от внешнего мира достигается через так называемые "порты" и "адаптеры", как показано на следующей диаграмме:

Hexagonal architecture with business logic in the core (“application”), ports, adapters, and external components Шестигранная архитектура с бизнес-логикой в центре (“приложение”), портами, адаптерами и внешними компонентами

Бизнес-логика ("приложение") находится в центре архитектуры. Она определяет интерфейсы ("порты") для взаимодействия с внешним миром – как для управления (посредством API, пользовательского интерфейса, других приложений), так и для управления (базой данных, внешними интерфейсами и другой инфраструктурой).

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

Следующая иллюстрация показывает пример приложения, которое:

  1. Управляется пользователем через пользовательский интерфейс,
  2. Управляется пользователем через REST API,
  3. Управляется внешним приложением через тот же REST API,
  4. Управляет базой данных и
  5. Управляет внешним приложением.

(Нумерация не представляет порядок, а ссылается на стрелки на иллюстрации).

Hexagonal architecture with control flow Шестигранная архитектура с потоком управления

Связь с внешними компонентами обеспечивается "адаптерами".

Например, пользовательский интерфейс может предоставить форму регистрации. Когда пользователь заполняет все данные и нажимает "Регистрация", адаптер UI генерирует команду "Зарегистрировать пользователя" и отправляет её в бизнес-логику. Альтернативно, та же команда может быть сгенерирована адаптером REST для соответствующего HTTP POST запроса:

Hexagonal architecture: port with user interface and REST adapter Шестигранная архитектура: порт с пользовательским интерфейсом и адаптером REST

С другой стороны приложения, адаптер базы данных может переводить команду "Сохранить пользователя" в SQL-запрос "INSERT INTO User VALUES (…)":

Hexagonal architecture: port with a database adapter Шестигранная архитектура: порт с адаптером базы данных

Как именно адаптер это делает – использует ли он ORM и какой версии – не имеет значения с точки зрения ядра приложения.

Несколько адаптеров могут быть подключены к одному порту. Например, как в приведенном выше примере, к порту для управления приложением могут быть подключены адаптер пользовательского интерфейса и адаптер REST. И к порту для отправки уведомлений могут быть подключены адаптеры электронной почты, SMS и WhatsApp.

Кстати, термин "порт" относится к электрическим соединениям, к которым можно подключать любые устройства, соответствующие

механическим и электрическим протоколам соединения.

Первичные и вторичные порты и адаптеры

Из приведенного выше примера видно, что существуют два типа портов и адаптеров – те, которые управляют приложением, и те, которые управляются приложением.

Мы называем первую группу "первичными" или "управляющими" портами и адаптерами; они обычно показываются на левой стороне шестиугольника.

Вторую группу мы называем "вторичными" или "управляемыми" портами и адаптерами, обычно показываемыми на правой стороне.

Правило зависимости

Теоретически это звучит довольно хорошо. Но как мы программно обеспечим, чтобы никакие технические детали (например, JPA-сущности) и библиотеки (например, ORM) не проникали в приложение?

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

Hexagonal architecture: dependency rule Шестигранная архитектура: правило зависимости

Классы и их взаимосвязи довольно просты для первичных портов и адаптеров (то есть левой стороны изображения).

Оставаясь с примером регистрации пользователя, мы могли бы реализовать желаемую архитектуру с такими классами:

Hexagonal architecture: class diagram of primary port and adapter Шестигранная архитектура: диаграмма классов первичного порта и адаптера

RegistrationController является адаптером, интерфейс RegistrationUseCase определяет первичный порт, а RegistrationService реализует функциональность, описанную портом. (Я взял эту соглашение об именах из отличной книги "Get Your Hands Dirty on Clean Architecture" Тома Хомбергса).

Зависимость исходного кода идет от RegistrationController к RegistrationUseCase, как и требовалось, по направлению к ядру.

Но как мы реализуем вторичные порты и адаптеры, то есть правую сторону изображения, где зависимость исходного кода должна быть противоположна направлению вызова? Например, как ядро приложения может получить доступ к базе данных, если база данных находится за пределами ядра, а зависимость исходного кода должна быть направлена к центру?

Здесь вступает в силу принцип инверсии зависимости.

Инверсия зависимости

Также порт определяется интерфейсом. Однако взаимосвязи между классами меняются: PersistanceAdapter не использует PersistencePort, а реализует его. А RegistrationService не реализует PersistencePort, а использует его:

Hexagonal architecture: class diagram of secondary port and adapter Шестигранная архитектура: диаграмма классов вторичного порта и адаптера

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

Отображение

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

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

Dependency rule: dependencies from the core to the adapter are not allowed Правило зависимости: зависимости от ядра к адаптеру не допускаются

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

Dependency rule: dependencies from the core to the adapter are not allowed Правило зависимости: зависимости от ядра к адаптеру не допускаются

Как мы можем решить эту дилемму?

В следующих разделах я представлю вам различные стратегии для этого.

Дублирование с двусторонним отображением

Мы создаем дополнительный класс модели в адаптере, который не содержит никакой бизнес-логики, но содержит технические аннотации. Адаптер должен затем маппировать класс модели ядра на класс модели адаптера и наоборот.

Hexagonal architecture: duplication with two-way mapping Шестигранная архитектура: дублирование с двусторонним отображением

На моем опыте, этот вариант наиболее подходит.

Дублирование с односторонним отображением

Мы определяем интерфейс в ядре и позволяем как классу модели ядра, так и классу модели адаптера реализовать этот интерфейс. Таким образом, только модель, исходящая из ядра, нужно будет переводить в модель адаптера. Перевод в направлении ядра не требуется: адаптер может отправить свою собственную модель в ядро, поскольку она реализует интерфейс ядра.

Hexagonal architecture: duplication with one-way mapping Шестигранная архитектура: дублирование с односторонним отображением

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

Технические инструкции вне кода программы

Некоторые библиотеки, такие как Hibernate, позволяют определять технические инструкции в XML-файле вместо использования аннотаций в классе модели. Это позволяет адаптеру использовать класс модели ядра без дублирования кода.

Hexagonal architecture: technical instructions outside the program code Шестигранная архитектура: технические инструкции вне кода программы

Однако внешние инструкции часто гораздо более запутанные, чем аннотации в коде, поэтому я не особенно стремлюсь использовать эту стратегию.

Ослабление архитектурных границ

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

Hexagonal architecture: weakening architectural boundaries Шестигранная архитектура: ослабление архитектурных границ

Я всегда советую избегать этого варианта. Как только вы начнете с этого, не пройдет много времени – согласно теории разбитых окон – до того, как будет нарушено следующее архитектурное правило.

Отображение в адаптере REST

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

Мы также можем контролировать это с помощью технических аннотаций (например, @JsonIgnore или @JsonFormat при использовании Jackson). Но мы не хотим их в ядре приложения. Поэтому, даже с адаптерами REST, обычно имеет смысл маппировать сущность на специфический для адаптера класс модели, который содержит только видимые поля и инструкции по форматированию.

Тесты

В начале статьи я упомянул "изолируемые тестируемые компоненты" как одно из требований к хорошей программной архитектуре. Фактически, шестигранная архитектура очень упрощает для нас (как вы увидите на практике в следующих частях этой серии статей) тестирование бизнес-логики приложения:

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

Следующая диаграмма показывает модульный тест, который создает тестовый двойник

для базы данных и подключает его к вторичному порту базы данных ("Arrange"), вызывает случай использования на первичном порту ("Act") и проверяет ответ порта и взаимодействие с тестовым двойником ("Assert"):

Hexagonal architecture: unit tests for the business logic Шестигранная архитектура: модульные тесты для бизнес-логики

Бизнес-логику можно тестировать в изоляции от адаптеров, а адаптеры можно тестировать в изоляции от бизнес-логики (например, в экосистеме Java, первичные адаптеры REST с REST Assured, вторичные адаптеры REST с WireMock и адаптеры базы данных с TestContainers).

Следующая диаграмма показывает интеграционный тест, который создает тестовый двойник для первичного порта ("Arrange"), отправляет HTTP POST запрос на адаптер REST через REST Assured ("Act") и, наконец, проверяет HTTP ответ и взаимодействие с тестовым двойником ("Assert"):

Hexagonal architecture: integration test for a REST adapter Шестигранная архитектура: интеграционный тест для адаптера REST

Последняя диаграмма показывает интеграционный тест для адаптера базы данных, который использует TestContainers для запуска тестовой базы данных ("Arrange"), вызывает метод на адаптере базы данных ("Act") и, наконец, проверяет, соответствуют ли возвращаемое значение метода и, если применимо, изменения в тестовой базе данных ожиданиям ("Assert"):

Hexagonale Architektur: Integrationstest für Datenbank-Adapter Шестигранная архитектура: интеграционный тест для адаптера базы данных

В дополнение к этим изолированным тестам, полные системные тесты также не должны отсутствовать (в меньшей степени, согласно тестовой пирамиде).

Почему шестиугольник?

Алистер Кокберн часто спрашивают, имеет ли шестиугольник или число "шесть" какое-то особое значение. Его ответ на этот вопрос: "Нет". Он хотел использовать форму, которую никто раньше не использовал. Квадраты используются везде, а пятиугольники сложно рисовать. Поэтому он выбрал шестиугольник.

Шестиугольник также отлично подходит для рисования двух первичных портов слева и двух вторичных портов справа. Кокберн говорит, что никогда не сталкивался с проектом, которому требовалось бы больше четырех портов для его схематического представления.

Преимущества шестигранной архитектуры

Теперь, когда мы рассмотрели шестигранную архитектуру со всех сторон, пора вспомнить цели хорошей программной архитектуры и проверить, насколько шестигранная архитектура соответствует этим целям.

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

Рассмотрим эти критерии подробно.

Модифицируемость

  • Мы можем изменять бизнес-логику в ядре приложения без необходимости изменять адаптеры или инфраструктуру (хотя на практике изменение бизнес-логики часто включает изменения в пользовательском интерфейсе и хранилище данных).
  • Мы можем обновлять и заменять инфраструктуру (например, базу данных или ORM), не изменяя ни одной строки кода в бизнес-логике. Нам нужно только адаптировать соответствующий адаптер.
  • Начав с разработки ядра приложения, мы можем отложить решения о инфраструктуре и принять их очень поздно в процессе разработки. Опыт, полученный в ходе разработки ядра, позволяет принимать более обоснованные решения о выборе инфраструктуры (фреймворк приложения, система баз данных и т.д.).

Изоляция

  • Ядро приложения занимается исключительно бизнес-вопросами.
  • Все технические вопросы реализуются в первичных и вторичных адаптерах.
  • Ядро приложения и адаптеры изолированы портами – случаи использования в ядре приложения взаимодействуют исключительно с этими портами, не зная технических деталей за ними.
  • Изоляция позволяет все обязанности четко локализовать в коде, что значительно снижает риск размывания архитектурных границ.

Разработка

  • После определения портов приложения работа над компонентами (ядро, пользовательский интерфейс, подключение к базе данных и т.д.) может быть легко разделена между несколькими разработчиками, парами или командами.

Тестируемость

  • Как показано выше, мы можем тестировать все компоненты в полной изоляции, используя тестовые двойники.

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

Недостатки шестигранной архитектуры

Реализация портов и адаптеров, а также выбранной стратегии отображения представляет собой немалые дополнительные усилия. Это быстро окупается для больших корпоративных приложений; для небольших приложений, таких как простой CRUD микросервис с минимальной бизнес-логикой, дополнительные усилия не стоят того.

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

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

Шестигранная архитектура и DDD (доменно-ориентированный дизайн)

В литературе часто встречаются изображения шестигранной архитектуры с "сущностями" и "случаями использования" или "сервисами" внутри шестиугольника приложения и/или с "доменом" или "доменной моделью" шестиугольника внутри шестиугольника приложения – примерно как на следующем рисунке:

Hexagonal architecture and DDD (domain-driven design) Шестигранная архитектура и DDD (доменно-ориентированный дизайн)

Фактически, шестигранная архитектура намеренно оставляет открытым, что находится внутри шестиугольника приложения. В увлекательном интервью Алистер Кокберн ответил на вопрос: "Что вы видите внутри приложения?" следующим образом: "Мне все равно – это не мое дело. Шестигранный дизайн-паттерн представляет собой единственное дизайнерское решение: "Оберните свое приложение в API и поместите тесты вокруг него."

Тем не менее, доменно-ориентированный дизайн (DDD) и шестигранная архитектура дополняют друг друга исключительно хорошо, потому что дисциплина тактического дизайна DDD идеально подходит для структурирования бизнес-правил внутри шестиугольника приложения.

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

Шестигранная архитектура и микросервисы

Шестигранная архитектура также подходит для реализации микросервисов, если они соответствуют двум критериям:

  • Они должны содержать бизнес-логику, а не быть чисто техническими. Например, микросервис, который регистрирует все события, на которые он подписан в шине событий в другую систему, имеет строго техническое назначение. Нет бизнес-логики, которую можно изолировать от технических деталей.
  • Они должны быть определенного размера. Для микросервиса с минимальной бизнес-логикой дополнительные усилия для портов, адаптеров и отображения не стоят того. Нет фиксированного ограничения размера; решение должно быть принято на основе опыта. При использовании доменно-ориентированного дизайна и микросервис включает агрегат с несколькими сущностями и связанными сервисами, шестигранная архитектура обычно имеет смысл.

С точки зрения микросервиса, все другие микросервисы являются частями внешнего мира и изолируются через порты и адаптеры, так же как и остальная инфраструктура:

Hexagonal architecture and microservices Шестигранная архитектура и микросервисы

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

  • Использование стратегического дизайна для планирования основного домена, поддоменов и ограниченных контекст

ов. - Разделение ограниченного контекста на один или несколько микросервисов. Микросервис может содержать один или несколько агрегатов – но также и весь ограниченный контекст, пока он не слишком велик (и приложение не становится монолитом вместо микросервиса). - Реализация шестиугольника приложения в соответствии с тактическим дизайном, то есть с сущностями, объектами-значениями, агрегатами, сервисами и т.д.

Шестигранная архитектура vs. "Порты и адаптеры"

Шестигранная архитектура и "порты и адаптеры" (иногда "порты & адаптеры") относятся к одной и той же архитектуре. Официальное название, данное Алистером Кокберном для архитектурного паттерна, описанного в этой статье, – "порты и адаптеры".

Более распространенное, фигуральное название "шестигранная архитектура" происходит от графического представления архитектуры с использованием шестиугольников. Алистер Кокберн в вышеупомянутом интервью заявил, что он также предпочитает фигуральное название – но официальное название паттерна должно быть таким, которое описывает его свойства.

Шестигранная архитектура vs. Слоистая архитектура

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

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

Hexagonal architecture vs. layered architecture Шестигранная архитектура vs. слоистая архитектура

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

Многие из нас, разработчиков, работали с моделью слоев так долго, что она стала второй натурой, и мы считаем это самым нормальным в мире – планировать приложение вокруг базы данных.

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

И редко когда бизнес-приложение остается таким же простым, как показано выше. Как только приложение становится более сложным, создаются дополнительные зависимости. Следующая фигура показывает архитектуру, расширенную REST API и подключением стороннего сервиса:

Hexagonal architecture vs. layered architecture: extending the application Шестигранная архитектура vs. слоистая архитектура: расширение приложения

С шестигранной архитектурой четко определено, где находятся дополнительные компоненты.

В слоистой архитектуре REST API часто прикрепляется к бизнес-логике (возможно, дублируя бизнес-логику, реализованную в слое представления, если она не была предварительно рефакторингована), и бизнес-логика, в свою очередь, получает дополнительную зависимость от внешнего сервиса.

Я нарисовал "сторонний клиент" пунктирной линией, так как этот компонент часто опускается, и интерфейс внешнего приложения используется напрямую из бизнес-слоя (если не из слоя представления).

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

Hexagonal architecture vs. layered architecture: source code dependencies Шестигранная архитектура vs. слоистая архитектура: зависимости исходного кода

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

Шестигранная архитектура vs. Чистая архитектура

"Чистая архитектура" была представлена в 2012 году Робертом Мартином ("Дядя Боб") в его блоге Clean Coder и подробно описана в книге 2017 года "Clean Architecture".

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

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

Следующая диаграмма показывает шестигранную архитектуру и чистую архитектуру рядом:

Hexagonal architecture vs. clean architecture Шестигранная архитектура vs. чистая архитектура

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

Hexagonal architecture vs. clean architecture (both “normalized”) Шестигранная архитектура vs. чистая архитектура (обе "нормализованы")

Шестиугольники можно почти один к одному сопоставить с кольцами чистой архитектуры:

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

Порты явно не упоминаются в чистой архитектуре, но также присутствуют в связанных UML-диаграммах и примерах исходного кода в виде интерфейсов:

Interfaces: implicit “ports” in the clean architecture Интерфейсы: неявные "порты" в чистой архитектуре

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

Шестигранная архитектура vs. Луковая архитектура

Также в "луковой архитектуре", представленной Джеффри Палермо в его блоге в 2008 году, бизнес-логика находится в центре, в так называемом "ядре приложения". Ядро имеет интерфейсы к пользовательскому интерфейсу и инфраструктуре (база данных, файловая система, внешние системы и т.д.), но не знает их конкретные реализации. Таким образом, ядро также изолировано от инфраструктуры.

Как и в шестигранной и чистой архитектуре, все зависимости исходного кода указывают в направлении ядра. Где направление вызова идет противоположно зависимости исходного кода, применяется инверсия зависимости.

На следующем рисунке

вы можете увидеть сравнение шестигранной архитектуры и луковой архитектуры:

Hexagonal architecture vs. onion architecture Шестигранная архитектура vs. луковая архитектура

Если мы снова подкорректируем цвета и заменим пользовательский интерфейс, тесты и инфраструктуру на заполнители в луковой архитектуре и скроем необязательные кольца ядра приложения, мы снова получим две очень похожие картинки:

Hexagonal architecture vs. onion architecture (both “normalized”) Шестигранная архитектура vs. луковая архитектура (обе "нормализованы")

Шестиугольники можно почти один к одному сопоставить с кольцами луковой архитектуры:

  • "Внешние агентства", расположенные вокруг внешнего шестиугольника, представлены в луковой архитектуре инфраструктурными компонентами в нижнем правом углу.
  • Внешний шестиугольник "адаптеры" соответствует кольцу, содержащему "пользовательский интерфейс", "тесты" и "инфраструктуру".
  • Шестиугольник приложения соответствует ядру приложения в луковой архитектуре. Это далее подразделяется на "сервисы приложения", "доменные сервисы" и "доменную модель", причем только "доменная модель" является фиксированным компонентом луковой архитектуры. Другие кольца ядра приложения явно обозначены как необязательные. "Доменная модель" определяет "корпоративные бизнес-правила" и таким образом соответствует кольцу "сущности" – то есть самому внутреннему кругу – чистой архитектуры.

В конечном итоге, луковая архитектура также почти идентична шестигранной архитектуре – она отличается только явной "доменной моделью" в центре ядра приложения.

Резюме и перспективы

Шестигранная архитектура или "порты и адаптеры" (альтернативно чистая архитектура или луковая архитектура) – это архитектурный паттерн, который устраняет проблемы традиционной слоистой архитектуры (утечка технических деталей в другие слои, плохая тестируемость) и позволяет откладывать и изменять решения о технических деталях (например, используемой базе данных) без необходимости адаптации ядра приложения.

Бизнес-код находится в ядре приложения, остается независимым от технического кода в инфраструктуре и может разрабатываться и тестироваться в изоляции.

Все зависимости исходного кода указывают исключительно в направлении ядра. Принцип инверсии зависимости применяется, когда поток управления идет в противоположном направлении, то есть от ядра к инфраструктуре (например, к базе данных).

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

Эта статья является первой из серии многозначных статей. В следующих частях я покажу вам:

  • Как реализовать шестигранную архитектуру с Java – без фреймворка приложений, такого как Spring или Quarkus?
  • Как вы можете обеспечить соблюдение архитектурных спецификаций?
  • Как подключить порт хранения, уже подключенный к адаптеру памяти, к дополнительному адаптеру базы данных?
  • Как реализовать шестигранную архитектуру с Quarkus?
  • Как реализовать шестигранную архитектуру с Spring Boot?

Если вам понравилась статья, пожалуйста, поделитесь ею с помощью одной из кнопок для совместного использования в конце.

Хотите быть в курсе и получать уведомления о новых статьях на HappyCoders.eu? Тогда нажмите здесь, чтобы подписаться на бесплатную рассылку новостей HappyCoders. ```