Skip to content

Hexagonal Architecture

References:

Hexagonal Architecture

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

Традиционная архитектура

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

3-tier architecture

Presentation Layer

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

Logic Layer

Второй уровень — это Logic Layer (логический уровень) или Business Logic Layer (уровень бизнес логики). Как следует из названия, это основная часть вашего приложения, где находится вся логика вашей системы.

Data Layer

Наконец, у нас есть Data Layer (уровень данных) или Data Access Layer (уровень доступа к данным), который управляет тем, как данные сохраняются в вашем приложении.

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

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

Если вы не слышали о Hexagonal Architecture (шестиугольная архитектура), то, возможно, слышали о паттерне Ports & Adapters (порты и адаптеры).

Ports

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

Adapters

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

Driving (Primary) vs Driven (Secondary)

Входные данные заставляют наше приложение что-то делать, а выходные данные создаются самим приложением.

Почему это называется Hexagonal Architecture

Шестиугольник — это просто форма, помогающая визуализировать архитектуру, и не имеет реальных оснований в реальном мире.

Тем не менее, что вы думаете, когда видите шестиугольник?

Приложение может иметь множество входов и портов, и один из них может быть API. Когда мы сохраняем данные, мы обычно думаем о сохранении их в базу данных или файловую систему. Если у вас есть особенно большое приложение, можно начать делить его на разные domains (домены). Это концепция domain-driven design (предметно-ориентированного проектирования), где каждое приложение отвечает только за один домен. Один домен может быть управлением пользователями, другой — поиском или сохранением данных. Каждая из этих частей вашего приложения может стать собственным шестиугольником.

Когда использовать Hexagonal Architecture

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

Концепция Hexagonal Architecture

Hexagonal представляет business logic (бизнес-логику). Ему не важны ваш фреймворк разработки, ваш технологический стек и язык программирования. Это помогает вам определить decoupled (зацепленную) структуру кода, isolated (изолированную) от внешних компонентов, и easy-to-run tests (облегчает выполнение тестов).

Архитектура включает несколько важных компонентов. Скажем, у меня уже есть GUI (веб-фронтенд), и моя задача — разработать бэкенд-сервис. Рассмотрим следующую диаграмму последовательности:

Sign up flow

На диаграмме четыре компонента: пользователь, веб-интерфейс, User service и база данных. Пользовательский сервис предоставляет HTTP POST API /signup для веб-интерфейса. Внутренний пользовательский сервис включает три компонента:

  • REST Controller: получает запросы от веб-интерфейса через API и передает их обработчику для выполнения бизнес-логики.
  • Handler: валидирует запросы и выполняет логику. Если бизнес-данные нужно сохранить в базу данных, передает данные в репозиторий. В этом примере он сохраняет нового пользователя при регистрации.
  • Repository: преобразует бизнес-данные в объекты данных и отправляет их в базу данных через протокол базы данных. В этом примере он отправляет оператор INSERT в базу данных при создании нового пользователя.

Теперь я преобразую вышеуказанную диаграмму в Hexagonal Architecture.

Sign up flow with Hexagonal architecture

Компоненты User service располагаются по краям шестиугольника. Легко увидеть, что они разделены. Поскольку у них разные роли, они не должны зависеть друг от друга. Когда компонент изменяется, другие компоненты не должны быть затронуты. Как это сделать?

Продолжим преобразование вышеуказанной Hexagonal Architecture для User service в стандартную Hexagonal Architecture.

Hexagonal Architecture

Ports

Application (Приложение) — это ваша бизнес-логика как основная архитектура. Оно определяет Ports (интерфейсы) для взаимодействия с внешними компонентами (специфическими технологиями / внешними компонентами), такими как веб-интерфейс, база данных и т.д. Бизнес-логика знает только эти порты, которые представляют конкретные случаи использования логики. Существует два типа портов:

  1. Primary Port / Driver Port (Первичный порт / ведущий порт): предоставляет функции приложения внешнему компоненту. Мы можем назвать их границами использования приложения или API приложения. Например, он предоставляет функцию регистрации под названием sign-up handler (обработчик регистрации).
  2. Secondary Port / Driven Port (Вторичный порт / ведомый порт): предоставляет функции внешних компонентов для приложения, которое реализует бизнес-логику. Например, он предоставляет функцию создания записи в базе данных под названием sign-up repository (репозиторий регистрации).

Adapters

Компонент для соединения портов с внешними компонентами называется adapter (адаптером).

Существует два типа адаптеров:

  1. Primary adapter / Driver adapter (Первичный адаптер / ведущий адаптер)
  2. Secondary adapter / Driven adapter (Вторичный адаптер / ведомый адаптер)

Primary adapter / Driver adapter

Primary adapter / Driver adapter (Первичный адаптер / ведущий адаптер): использует интерфейс порта или запускает функцию порта для выполнения бизнес-логики. Он также преобразует запросы от внешних компонентов в запросы application (приложения).

В вышеуказанном примере REST-контроллер является первичным адаптером. Он определяет API POST /signup для вызова веб-интерфейса. Когда веб-интерфейс отправляет POST-запрос к API /signup, REST-контроллер преобразует запрос и отправляет его в бизнес-логику через sign-up handler (первичный порт).

Primary port and adapter

Secondary adapter / Driven adapter

Secondary adapter / Driven adapter (Вторичный адаптер / ведомый адаптер): реализует вторичный порт. Application (приложение) использует его для связи с внешними компонентами. Он преобразует запросы бизнес-логики в запросы внешних технологических компонентов.

В вышеуказанном примере логика регистрации требует создания новой записи в базе данных. Она вызывает репозиторий (вторичный порт) для создания новой записи, и репозиторий MySQL преобразует этот запрос в SQL-запрос INSERT INTO user (id, username, displayname, created_at) VALUES (1, 'yes', 'red', 123456789).

Secondary port and adapter

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

Multiple adapter

Actor

Существует два типа actors (акторов):

  1. Primary actor / Driver actor (Первичный актор / ведущий актор): использует функции application (приложения) или взаимодействует с приложением для достижения бизнес-цели, такие, как веб-интерфейс, мобильное приложение, человек (клиент), внешние приложения и т.д.
  2. Secondary actor / Driven actor (Вторичный актор / ведомый актор): предоставляет функции для использования application (приложения) или запускается application (приложения) для реализации бизнес-логики. Существует два типа ведомых акторов:
    • Repository (Репозиторий): предоставляет функции чтения и записи / отправки для приложения, такие как база данных, кэш, очередь и т.д.
    • Recipient (Получатель): предоставляет функции отправки для приложения, такие как почтовый сервер, SMTP-сервер, очередь и т.д.

Driver and driven actor

Как создать структуру кода на основе Hexagonal Architecture

Dependency pattern

Упомянуты два шаблона, которые используются в Hexagonal Architecture:

  • Adapter Pattern (шаблон адаптера)
  • Dependency Injection (внедрение зависимости)

Они помогают сделать архитектуру flexible (гибкой ) and decoupled (разъединенной) между компонентами. Как они используются в архитектуре? Начнем с ведущей стороны. На этой стороне есть четыре связанных компонента: actor, adapter, port и application (бизнес-логика). Важно, чтобы внешние компоненты взаимодействовали с application только через ports и adapters.

Dependency of driver side

Класс SignUpController использует интерфейс SignUpHandler для выполнения функции регистрации. SignUpServiceявляется реализацией SignUpHandler. Он реализует функцию регистрации, определенную в интерфейсе SignUpHandler. Между классом SignUpController и интерфейсом SignUpHandler появляется зависимость кода. Это способ реализовать primary adapter и primary port для application.

На ведомой стороне также есть четыре связанных компонента: actor, adapter, port и application (бизнес-логика), но роли инвертированы. Приложение взаимодействует с внешними компонентами через порты и адаптеры.

Dependency of driven side

Класс SignUpService использует интерфейс Repository для создания новой записи. MySQLRepository является экземпляром или реализацией Repository. Он определяет способ создания записей в базе данных MySQL. Между классомSignUpService и интерфейсом Repository появляется зависимость кода.

В общем, можно заметить, что в Hexagonal Architecture есть dependency inversion (инверсия зависимости).

Рассмотрим приведенную ниже модель:

Dependency inversion

Передача данных между компонентами

Благодаря dependency pattern, Hexagonal Architecture разделяет компоненты. Как эти компоненты взаимодействуют между собой? Язык является средством общения между людьми. Компоненты в структуре кода взаимодействуют друг с другом посредством data objects (объектов данных).

Адаптер является communication bridge (коммуникационным мостом) между внешними компонентами и application (приложением). Поэтому он выполняет роль преобразования типа данных объекта, который могут понять компоненты.

Например, на driver side SignUpController (primary adapter) получает запрос от веб-интерфейса, он должен преобразовать запрос в пользовательский ввод перед отправкой его в SignUpHandler. При получении результата отSignUpHandler он также преобразует пользовательский вывод в ответ, который может понять веб-интерфейс.

Mapping data object from primary adapter

Эту же идею мы используем на driven side. MySQLRepository (adapter) преобразует data objects между SignUpService и базой данных MySQL.

Mapping data object from secondary adapter

Затраты

Преобразование data objects достаточно затратно с точки зрения использования памяти и производительности сервиса. Но это помогает сохранять decoupled компонентов в сервисе. Если ваш сервис не меняет технологию часто, вы можете повторно использовать data objects между adapter и компонентом port.

Например, SignUpController (adapter) и SignUpHandler могут использовать один и тот же SignUpRequest и SignUpResponse. Это означает, что SignUpController отправляет SignUpRequest в SignUpHandler, затемSignUpHandler возвращает SignUpResponse в SignUpController.

Reuse data object

Как написать тест с использованием Hexagonal Architecture

Тестирование — важный этап в цикле разработки проекта. Благодаря изоляции, которую предоставляет Hexagonal Architecture, структура кода проекта очень легко тестируется. Application управляет бизнес-логикой, что является важной ролью проекта. Поэтому мы сначала пишем тесты для этого компонента.

Тестирование компонента application

Application не заботится о внешних компонентах (веб-интерфейс, база данных и т.д.), поэтому мы применяем test double (тестовый дублер) для внешних компонентов. Это означает, что нам нужно:

  • Написать unit tests (модульные тесты) для запуска application с primary port.
  • Реализовать mock instance для secondary adapter.

Application unit test

При написании модульных тестов для бизнес-логики, вы должны знать expected result (ожидаемый результат) этих тестов и сравнивать их с actual result (фактическим результатом) при запуске этих тестов. Нам нужно имитировать подходящее состояние для каждого теста (т.е. задавать разное поведение для mocks).

Например, вы хотите написать unit test для случая успешной регистрации в SignUpService. Вам нужно настроить так, чтобы MockRepository возвращал успешный результат, когда SignUpService вызывает Repository для создания нового пользователя. Затем вы сравниваете этот результат с ожидаемым результатом. В противном случае вы также возвращаете duplicate user error (ошибку дублирования пользователя) для неудачного test case.

Тестирование компонента adapter

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

Тестирование primary adapter

Используйте инструменты для end-to-end HTTP и REST API testing для запуска API первичного адаптера, в случае когда он представляет собой REST Controller. Например для Go можно применить httpexpect (применяйте HTTP REST контроллер) Для primary adapters другого типа используйте другой подходящий инструментарий.

Существует два способа выполнить тестирование primary adapter:

  • Оставьте MockRepository как в приведенном выше примере и сравните результат на уровне primary adapter.
  • Реализуйте MockSignUpService и настройте подходящее состояние для каждого теста.

Primary adapter unit test

Затем настройте unit tests для secondary adapter по следующей модели:

Secondary adapter unit test

Преимущества и недостатки

При чтении вышеуказанных разделов вы также осознаете преимущества и недостатки Hexagonal Architecture для программных проектов, особенно для бэкенд-проектов.

Преимущества

Isolation (Изоляция)

Isolation

Упомянута изоляционная функция. Это особенность, которая отличает Hexagonal Architecture. Она разделяет компоненты в структуре кода: бизнес-логику, внешний компонент и технологический стек. Изоляция также помогает снизить риск для вашего проекта при смене технологического стека. Если смена технологического стека вызывает проблему, вы просто переключаете использование старого адаптера.

Flexibility (Гибкость)

Flexibility

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

Testability (Тестируемость)

Testability

Написание тестов для приложения очень легко выполнить. Компоненты изолированы друг от друга, поэтому мы можем писать изолированные unit tests для каждого компонента. Кроме того, мы можем использовать механизм test double для поддержки тестирования.

Development and maintainability (Разработка и поддержка)

Development and maintainability

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

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

Недостатки

Complexity (Сложность)

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

Mapping (Преобразование)

Затраты на преобразование объектов данных должны учитываться при создании изоляции между компонентами, это trade-off (компромисс) для этой задачи.

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