Эволюция серверной архитектуры: n-слойная, DDD, шестиугольная, луковичная, чистая

Хотите стать архитектором? А лучшим разработчиком? Для этого не нужно изучать все архитектуры и работать с каждой. Чтобы выделяться на фоне других разработчиков, достаточно знать историю и особенности применения хотя бы самых популярных архитектур: n-слойной, DDD, шестиугольной, луковичной, чистой.

Заинтересовались? Тогда начнем.

С чего все началось

В старые добрые времена не было никакой архитектуры. Если вы знаете шаблоны GoF, то вы уже архитектор.

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

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

На какое-то время проблема решилась, но не совсем. Если вы из сообщества C#, как и я, то наверняка думаете, что желтый прямоугольник Model на этих схемах  —  это просто объекты переноса данных DTO. Ошибаетесь, и все из-за Microsoft: мы сбиты с толку их MVC-фреймворком ASP. Они там что, считают себя умнее меня только потому, что я глупее?

На самом деле Model  —  это модель предметной области, или бизнес-логика, она важна в любом приложении.

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

На тот период шаблонов GoF стало просто недостаточно. И появились новые идеи. Как справиться со сложностью? Верно: следуя принципу «разделяй и властвуй». С MVC мы это уже делали, сделаем еще.

2002 год: n-слойная

Идеальная архитектура не появляется из ниоткуда: был пройден путь проб и ошибок.

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

Мартин Фаулер: «Дайте приготовить».

И ему дали приготовить.

В своей книге Patterns of Enterprise Application Architecture («Шаблоны корпоративных приложений») он описал n-слойную архитектуру.

Идея проста: разбить весь код на слои и назвать их.

Но это еще не все. Фаулер знает, насколько вредна непоследовательность. Поэтому он ввел ограничения. Можно:

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

Так разработчики структурировали код и избавились от его дублирования.

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

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

Разноцветные прямоугольники здесь  —  это слои, просто проекты в решении, а стрелки  —  зависимости между ними.

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

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

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

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

Сейчас это очевидно, но тогда было совсем непросто. Применять всегда легче, чем придумывать.

2003 год: предметно-ориентированное проектирование

В 2003 году молодой, 46-летний разработчик из Бостона Эрик Эванс опубликовал книгу Domain-Driven Design: Tackling Complexity in the Heart of Software («Предметно-ориентированное проектирование. Структуризация сложных программных систем»), после чего по крайней мере один Мартин в мире сильно приуныл.

Но DDD  —  это отдельная тема, поэтому опишем пока только добавляемые ею изменения архитектуры.

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

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

А главное, Эванс сказал: «Забудьте о базе данных, бизнес-логика важнее». Сказать-то сказал, но в архитектуре мало что изменил.

В его архитектуре определены слои:

  • Представления  —  для взаимодействия с пользователями.
  • Приложения  —  для координации задач и делегирования работы объектам предметной области.
  • Предметной области  —  понятия предметной области: то, чем занимается приложение и чем оно уникально на фоне других.
  • Инфраструктуры  —  постоянное хранение данных в памяти и сохранение состояния приложения.

Как видите, он кое-что переименовал.

Под пользовательским интерфейсом подразумевается наличие пользователей, но они не всегда имеются. Иногда это графический интерфейс для пользователей или интерфейс командной строки для разработчиков, а чаще  —  API, или интерфейс прикладного программирования, для программ. Слой представления  —  это просто более общее годное название.

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

База данных не единственный внешний инструмент, поэтому все отправщики писем, шины событий, SQL и прочий мусор перенесли в уровень инфраструктуры.

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

2005 год: шестиугольная, порты и адаптеры

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

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

Первым увидел эту возможность Элистер Кокберн.

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

Много вращений и перемещений, но посмотрим, что там случилось на самом деле.

Кокберн воплотил мечту Эванса. Теперь предметная область  —  центральный компонент системы не на словах, а на деле. Он не ссылается на другие проекты.

Поэтому бизнес-логика переименована в сердцевину Core.

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

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

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

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

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

Одни плюсы.

Адаптеры, которыми вызывается система, называются первичными, или ведущими, а вызываемые системой  —  вторичными, или ведомыми.

Вот оптимальная для меня структура решения:

Что выбрать: папки или проекты  —  решать вам.

Следите, чтобы ссылки не перекрывались там, где не должны:

2008 год: луковая

Будет жутковато, так что приготовьтесь.

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

И обещание было исполнено: от его луковки миллионы разработчиков по всему миру рыдают и плачут.

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

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

Но правило простое: любой внешний слой зависит только от внутренних.

Не такое простое, если подумать. Тогда порежем этот лук:

В самом центре  —  предметная область. Внутренних слоев нет, поэтому от других слоев она не зависит.

Под приложением только предметная область  —  одна-единственная его зависимость.

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

Здесь имеются все модули архитектуры DDD, но с ними работают по-другому. И это очень важно. Здесь главное  —  наличие в середине редко изменяемых компонентов, которые часто являются источниками изменений других компонентов.

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

Так в теории. А на практике корень композиции, то есть функция Main(), где регистрируются все зависимости и объединяются модули,  —  это часть слоя представления с ASP, WPF, CLI, поэтому схема получается такой:

Знакомо? Это n-слойная архитектура, но с другим порядком компонентов.

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

2012 год: чистая архитектура

Написать статью об архитектуре, не упомянув Роберта С. Мартина, просто невозможно.

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

Шутка. Сейчас оригинальных идей не много, все воруют друг у друга. Посмотрим, что привнес сюда Мартин.

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

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

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

Строгих правил, опять же, нет. Слоев добавляется сколько угодно на любом уровне. Хотите определить слой служб предметной области? Вперед!

Рядом с архитектурой Мартин пририсовал схему.

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

Здесь подчеркивается, что поток выполнения  —  пунктир  —  не всегда соответствует направлению зависимостей  —  прямая. Это принцип инверсии зависимостей.

Фактически это та же инверсия управления, которую мы наблюдали при обсуждении портов и адаптеров.

Обычно в ASP нет отдельного компонента для презентера. Выполняется это и контроллером. Вся схема представлена в таком коде:

class OrderController : ControllerBase, IInputPort, IOutputPort
{
[HttpGet]
public IActionResult Get(int id)
{
_getOrderUserCase.Execute(id);

return DisplayOutputPortResult();
}
}

Другие формы изоляции

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

Считаете вы их «истинными» архитектурными подходами или нет, в один прекрасный день все равно воспользуетесь каким-либо из этих архитектурных стилей или даже их комбинацией:

Заключение

Мы рассмотрели самые известные архитектуры: n-слойную, DDD, шестиугольную, луковичную, чистую. Имеются и другие, например BCE, DCI.

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

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

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

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи iamprovidence: Backend side architecture evolution (N-layered, DDD, Hexagon, Onion, Clean Architecture)

Предыдущая статьяТри фактора сдерживания прогресса ИИ
Следующая статьяРазвертывание приложений Python в Azure