Чистая архитектура  —  это рекомендации по организации системной архитектуры. Они были предложены Робертом С. Мартином (известным также как Дядя Боб) и основаны на ряде прежних архитектурных построений, таких как гексагональная архитектура, луковая архитектура и т. д.

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

Зачем нужна архитектура?

“Задача архитектуры ПО  —  минимизация человеческих ресурсов при разработке и последующем сопровождении системы”,  —  Роберт С. Мартин.

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

Чистая архитектура

Вот иллюстрация чистой архитектуры, созданная Робертом Мартином.

Чистая архитектура

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

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

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

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

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

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

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

Чистая архитектура (вертикально)

Цветовые обозначения на обоих схемах идентичные.

Помните, что стрелка должна читаться как “зависит от”, т.е. Frameworks and Drivers должны зависеть от Interface Adapters, те в свою очередь зависят от Application Business Rules, а они зависят от Enterprise Business Rules.

Ни один нижний слой не должен зависеть от верхнего.

Frameworks and Drivers

В этом слое размещены программные компоненты:

  • User Interface (пользовательский интерфейс);
  • Database (база данных);
  • External Interfaces (внешние интерфейсы, например платформа API);
  • Web (например, сетевой запрос);
  • Devices (устройства, например принтеры и сканеры).

Interface Adapters

Слой интерфейсных адаптеров включает:

  • Presenters (логика и состояния пользовательского интерфейса);
  • Controllers (интерфейс с необходимыми приложению методами, которые реализованы через интернет (Web), устройства (Devices) или внешние интерфейсы (External Interfaces);
  • Gateways (интерфейс, включающий все операции типа CRUD (create, read, update, delete), выполняемые приложением с базой данных).

Application Business Rules (Правила логики функционирования приложения)

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

Кроме того, этот уровень определяет контроллер/шлюз (Controller / Gateway), вызываемые для конкретного варианта использования. Иногда нужны контроллеры из разных модулей.

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

В этом случае нужно получить из purchase module (модуля покупки) сумму, потраченную пользователем в этом месяце, а затем по результату применить скидку в checkout module (модуле оформления заказа). Здесь applyDiscountUseCase вызывает контроллер модуля покупки для получения данных, а затем применяет скидку в модуле оформления заказа.

Application Business Rules (Правила логики функционирования проекта)

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

На него не влияют изменения на любом внешнем (нижнем) уровне. Поскольку логика функционирования (Business Rules) меняться будет не часто, изменения на этом уровне происходят очень редко. Этот уровень включает Entities.

Entity (логический объект) может быть либо основной структурой данных, необходимой для правил логики функционирования, либо объектом с методами, содержащими логику функционирования.

Например, модуль Interest (вычисления процентов) в банковском приложении  —  это основная логика функционирования, которая должна находиться на этом уровне.


Разберемся на примере. Для этого создадим простое приложение с одним сетевым запросом. Оно выполняет перевод заданного пользователем предложения с помощью специального API . Посмотрим, как это можно сделать.

На каждом уровне выполняется определенная задача. Но все ли здесь логично? Проконтролируем направление зависимостей для этой архитектуры (структуры) в соответствии с правилом: “Зависимости в исходном коде должны иметь только внутренние направления”.

  • UI → Presenter (✅ Верно)
  • Presenter → Translate Usecase (✅ Верно)
  • Translate Usecase → Translate Controller (❌ Не верно)
  • Translate Controller → Web (❌ Не верно)

UI запрашивает данные от Presenter, который запрашивает данные от Use Case, а он должен запросить данные от Controller, который должен запросить от Web.

Можно ли ожидать, что Web передает какие-либо данные в Controller, а Use Case получит необходимые данные от Controller в нарушение правила зависимости? Ведь именно оно делает архитектуру работоспособной.

В соответствии с этим правилом некоторые стрелки здесь должны иметь другое направление. Возможно ли это? На помощь нам приходит способность полиморфизма функций.

Зависимость можно просто инвертировать, имея Interface (интерфейс) между этими двумя слоями. Это известный принцип инверсии зависимостей.

Реализуем этот принцип в случаях нарушений правила зависимости.

В результате получаем следующую схему.

Теперь проверим отсутствие нарушений в потоке зависимостей.

Теперь ни один внутренний уровень не зависит от внешних. Скорее, внешние зависят от внутренних.

Почему же так должно быть?

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

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

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

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

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Bharath: The Clean Architecture — Beginner’s Guide

Предыдущая статьяПочему стоит использовать Pathlib в качестве альтернативы модуля OS
Следующая статьяРазвертывание Cloud Functions в GCP с помощью Terraform