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

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

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

Чистое разделение

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

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

Соберите вместе вещи, которые меняются по одним и тем же причинам. Разделяйте вещи, которые меняются по разным причинам — принцип единой ответственности

Многоуровневая архитектура

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

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

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

Когда слои становятся лазаньей

Несмотря на четкое разделение многоуровневой архитектуры, границы не всегда хорошо соблюдаются.

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

Кроме того, многоуровневый подход намекает на одно измерение архитектуры. Что, если вам нужно больше, чем просто пользовательский интерфейс для управления приложением — добавление CLI, REST API или потока событий в качестве входных данных? Конечно, вы можете добавить их в многоуровневую архитектуру, но они не совсем соответствуют одномерной ментальной модели.

За рулем и за рулем

Давайте перевернем многоуровневую архитектуру на бок.

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

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

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

Шестиугольная архитектура / порты и адаптеры

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

Ключевое изменение, которое мы внесем, заключается в том, что зависимости будут указывать внутрь — на домен — а не на следующий уровень в цепочке, как раньше. Это позволяет нам внедрять специфику (базы данных, пользовательские интерфейсы и т. д.) по мере необходимости и оставлять наши основные абстракции чистыми и нетронутыми.

Абстракции не должны зависеть от деталей. Детали (конкретные реализации) должны зависеть от абстракций — принцип инверсии зависимостей

Приложение и домен

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

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

Порты во внешний мир

Конечно, в это ядро ​​нужно что-то воткнуть, иначе оно совершенно бесполезно. Здесь на помощь приходят порты.

Порт — это просто интерфейс. На стороне вождения этот интерфейс предназначен для вызова нашего программного обеспечения, чтобы задать ему вопросы — например, внести 10 долларов на мой банковский счет. На стороне управляемого у нас есть интерфейсы, которые будут вызываться доменом для использования внешних ресурсов — баз данных, внешних API и т. д.

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

Подключение с помощью адаптеров

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

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

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

Заключение

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

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

[править] Что делает эту архитектуру более устойчивой, чем эквивалентная многоуровневая архитектура?

Во-первых, инверсия зависимостей. Рассмотрим многоуровневую архитектуру:

Пользовательский интерфейс -> Домен -> Постоянство [Интерфейс]

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

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

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

UI -> Домен [Интерфейс] ‹- Постоянство

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

По сути, речь идет о защите/эволюции приложения с течением времени, а не об объективно лучшей архитектуре в идеальном мире (и это, безусловно, не обходится без компромиссов).

Компромиссы

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

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

Ресурсы