Как стать автором
Обновить

Разбираем архитектуру. Часть 1. Чистая архитектура и её корни: история и взаимосвязи

Уровень сложностиПростой
Время на прочтение24 мин
Количество просмотров4K

Предисловие

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

Я решил написать серию статей, посвящённых различным аспектам проектирования программных систем, но первоначальной идеей было показать архитектурное решение моего pet-проекта на FastAPI — пример реализации «чистой архитектуры» с использованием современного стека: Python3.13, FastAPI, Uvicorn, Nginx, PostgreSQL, Alembic, Celery, Redis, Pytest, Filebeat, Logstash, Elasticsearch, Kibana, Prometheus, Grafana, Docker и Docker Compose.

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

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

Как правильно разделять слои/уровни? Где заканчивается бизнес-логика и начинается инфраструктура? Что считать доменной сущностью и чем она отличается от DTO? И, в конце концов — как мой «говнокод» превратить во что-то похожее на чистую архитектуру?

Некоторые, услышав термины вроде «сервисный слой», просто перемещают код из views.py/api.py в services.py, считая, что теперь всё по канону. Логика отделена от представления, но не отделена от реализации, ну и ладно, работает же.

И так сойдет

Ситуацию осложняет то, что у «чистой архитектуры» нет единого стандарта. Я сейчас говорю не столько о книге «Чистая архитектура» Роберта Мартина, сколько про то, что считать чистой и правильной архитектурой. Есть общее понимание, но нет единого соглашения. Поискав, ты найдёшь десятки статей, книг и репозиториев с похожими или даже противоречивыми примерами. Кто-то называет сущности core, кто-то domain, кто-то entities, кто-то models, кто-то schemas, кто-то base. И каждый из них прав по-своему.

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

В следующей статье данного цикла я поделюсь небольшим FastAPI проектом, который я написал специально под эту тематику. Его цель — продемонстрировать архитектурные стандарты: соблюдение различных принципов, модульность, чёткие границы, осознанные имена модулей, классов, функций. Я опишу почему именно так были названы те или иные компоненты, сравню их семантическое значение и постараюсь найти золотую середину.

Определения, термины и важные понятия

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

Базовые архитектурные принципы:

  • Single Responsibility Principle (SRP) (Принцип единственной ответственности).
    Традиционно звучит как:
    «Модуль должен иметь одну и только одну причину для изменения»

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

    SPR касается функций и классов, но он проявляется в разных формах на еще двух более высоких уровнях. На уровне компонентов он превращается в принцип согласованного изменения (Common Closure Principle; CCP), а на архитектурном уровне — в принцип оси изменения (Axis of Change), отвечающий за создание архитектурных границ.

  • Open/Closed Principle (OCP) (Принцип открытости/закрытости). 
    Традиционно звучит как:
    «Программные сущности должны быть открыты для расширения и закрыты для изменения».

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

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

  • Liskov Substitution Principle (LSP) (Принцип подстановки Барбары Лисков).
    В оригинале описан с формулировкой определения подтипов:
    «Здесь требуется что-то вроде следующего свойства подстановки: если для каждого объекта o1 типа S существует такой объект o2 типа T, что для всех программ P, определенных в терминах T, поведение P не изменяется при подстановке o1 вместо o2, то S является подтипом T1»

    Если упросить, он может звучать как:
    «Объекты подклассов должны быть пригодны для замены объектов суперклассов без изменения поведения программы».

    Пример: IUserRepository.get_by_email должен вести себя одинаково во всех реализациях — всегда возвращать пользователя или кидать однотипное исключение.

  • Interface Segregation Principle (ISP) (Принцип разделения интерфейсов).
    «Клиенты не должны зависеть от интерфейсов, которые они не используют».

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

    Пример: один огромный интерфейс IStorage лучше разбить на IFileStorage, ICacheStorage, IDocumentStorage, если разные клиенты используют разные его части.

    Примечание: под «клиентами» здесь подразумеваются компоненты (обычно — другие классы, модули или сервисы), которые зависят от интерфейса и его используют. Они вызывают методы интерфейса.

  • Dependency Inversion Principle (DIP) (Принцип инверсии зависимости).
    «Модули верхних уровней не должны зависеть от модулей нижних уровней, а оба типа модулей должны зависеть от абстракций; сами абстракции не должны зависеть от деталей, а вот детали должны зависеть от абстракций».

    Формулировка Р. Мартина:
    «Код, реализующий высокоуровневую политику, не должен зависеть от кода, реализующего низкоуровневые детали. Напротив, детали должны зависеть от политики»
    .
    Политика воплощает все бизнес-правила и процедуры. Детали — это все остальное, что позволяет людям, другим системам и программистам взаимодействовать с политикой, никак не влияя на ее поведение.

    Примечание: стоит отметить, что именно следует считать высокоуровневым модулем, а что низкоуровневым, чтобы их не спутали со слоями в архитектуре.
    Термин «уровень» имеет строгое определение: «удаленность от ввода и вывода» (центральный/внутренний слой = высокоуровневый, а внешний слой = низкоуровневый):
    Высокоуровневый модуль (high-level module) — это модуль, содержащий бизнес-логику, правила системы, то есть policy (политику поведения).
    Низкоуровневый модуль (low-level module) — это детали реализации, технические средства, которые используются для реализации политики.

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

    Пример: use case зависит от IUserRepository, а не от PostgresUserRepository.

  • Reuse/Release Equivalence Principle (REP) (Принцип эквивалентности повторного использования и выпусков)
    «Единица повторного использования есть единица выпуска».

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

  • Common Closure Principle (CCP) (Принцип согласованного изменения)
    «В один компонент должны включаться классы, изменяющиеся по одним причинам и в одно время. В разные компоненты должны включаться классы, изменяющиеся в разное время и по разным причинам».

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

    Оба принципа можно привести к общей формуле:
    «Собирайте вместе все, что изменяется по одной причине и в одно время. Разделяйте все, что изменяется в разное время и по разным причинам».

  • Common Reuse Principle (CRP) (Принцип совместного повторного использования)
    «Не вынуждайте пользователей компонента зависеть от того, чего им не требуется».

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

  • Separation of Concerns (SoC) (Разделение обязанностей / сфер ответственности /аспектов поведения)
    Это принцип проектирования, при котором разные аспекты системы (UI, бизнес-логика, хранение данных, логгирование и т.д.) разделяются на независимые компоненты или слои, каждый из которых решает только свою задачу.

  • Axis of Change (AoC) (Принцип оси изменения)
    Изменения происходят вокруг оси, и это предполагает, что каждая обязанность действует как центральная точка для существования класса. Вы хотите, чтобы у класса была единственная причина для существования, чтобы изменениями было легче управлять.

  • Encapsulation (Инкапсуляция)
    Это сокрытие внутренних деталей реализации и ограничение доступа к ним. Её цель — минимизировать зону ответственности потребителя за внутреннее поведение, изолировать изменения и контролировать границы контракта.

    • Интерфейсы/абстракции (представляют только контракт)

    • Уровни доступа (private/protected)

    • Паттерны (например, facade, adapter, proxy)

  • Acyclic Dependency Principle (ADP) (Принцип ацикличности зависимостей)
    В формулировке Р. Мартина:
    «Циклы в графе зависимостей компонентов недопустимы»

    То есть граф зависимостей между модулями должен быть ацикличным — нельзя создавать круговые зависимости.

  • Stable Dependencies Principle (SDP) (Принцип устойчивых зависимостей)
    В формулировке Р. Мартина:
    «Зависимости должны быть направлены в сторону устойчивости»

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

  • Stable Abstractions Principle (SAP) (Принцип устойчивости абстракций)
    В формулировке Р. Мартина:
    «Устойчивость компонента пропорциональна его абстрактности».

Управление зависимостями:

  • Inversion of Control (IoC) (Инверсия управления)
    Это принцип, при котором создание объектов, управление их зависимостями и вызов методов делегируются внешнему механизму, а не контролируются внутри самих объектов.
    Иначе говоря, вы не вызываете зависимости напрямую, а отдаёте контроль для этого другому компоненту, например, фреймворку, DI-контейнеру или инфраструктуре.
    Реализации:

    • Dependency injection (через DI-фреймворки, DI-контейнеры, вручную)

    • Callbacks / Hooks / Event listeners

    • Abstract factory

    • Strategy pattern

    • Service Locator

  • Dependency Injection (DI) (Инъекция зависимостей)
    Это механизм предоставления зависимостей объекту извне.

    • Это один из способов реализовать DIP: зависимости внедряются через конструктор, методы или свойства, а не создаются внутри.

    • Все инъекции — это инверсии, но не всякая инверсия — это инъекция.

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

    Пример: репозиторий вместо прямого доступа к базе.

Основные архитектурные компоненты:

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

    Но в зависимости от контекста, это может быть почти что угодно в системе:

    • Класс (на уровне кода)

    • Пакет / модуль

    • Отдельный элемент интерфейса

    • Отдельное приложение / сервис

  • Interface (Интерфейс) - это абстрактное определение поведения, не зависящее от реализации. Тесно связан с такими понятиями как «контракт» и «сигнатура»:

  • Signature (Сигнатура) — это формальная структура: имя, аргументы, типы, возвращаемое значение, можно сказать, что это синтаксис.

  • Contract (Контракт) — это поведение и ожидания: «если переданы такие-то данные — произойдёт это», можно сказать, что это семантика.

  • Implementation (реализация) – это конкретный способ выполнения поведения, определённого интерфейсом или абстрактным описанием:

    • Реализация интерфейса — это самый технический и очевидный случай. Конкретный класс реализует интерфейс (или абстрактный класс), удовлетворяя его контракт.

    • Реализация логики (внутренняя) – это реализация поведения на уровне use-case или сервиса.

    • Реализация use-case (внедрённая) – это вариант, где use-case не только описан, но уже «сконфигурирован» с конкретными реализациями, т.е. мы реализуем сценарий в конечном виде, где конкретные зависимости уже подставлены.

  • Инвариант (в архитектуре) — это недопустимое для нарушения ограничение, соблюдение которого критично для целостности системы.
    Например:

    • Бизнес-правило, которое всегда должно выполняться (например, баланс счёта не может быть отрицательным).

    • Ограничение на архитектурную структуру (например, интерфейсы из слоя domain никогда не импортируют код из infrastructure)

Прочее:

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

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

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

    Пример: Модуль «аутентификация» содержит только сущности, интерфейсы и use-case'ы, относящиеся к соответствующему процессу.

  • Coupling (связанность / связывание / внешняя зависимость) – определяет степень зависимости одного компонента от других.

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

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

Истоки и базовые архитектурные подходы

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

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

При этом каждая из этих архитектур обладает рядом общих характеристик:

  • Независимость от фреймворков. Архитектура не зависит от наличия каких-либо библиотек или платформ – они рассматриваются лишь как детали. Это позволяет использовать фреймворки как инструменты, а не навязывать структуру приложения​.

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

  • Независимость от UI. Пользовательский интерфейс можно легко изменять, не затрагивая остальную систему. Например, веб-интерфейс можно заменить консольным интерфейсом, не изменяя бизнес-правил.

  • Независимость от БД. Можно сменить СУБД или вообще вместо БД использовать, к примеру, файлы – бизнес-логика не зависит от деталей хранения​.

  • Независимость от внешних агентов. Бизнес-правила ничего не знают о внешнем мире и окружении. ​

Историческая сводка:

  1. BCE (Boundary, Control, Entity) (Граница, Управление, Сущность) - 1992 г.

  2. Hexagonal Architecture (Гексагональная архитектура, «Порты и адаптеры») – 2005 г.

  3. Onion Architecture (Луковичная архитектура) – 2008 г.

  4. Clean Architecture (Чистая архитектура) – 2012 г.

  5. DCI (Data, Context, Interaction) (Данные, Контекст и Взаимодействие) – ~2009 г. – дополнение к любой архитектуре

  6. Screaming Architecture («Кричащая» архитектура) – 2011 г. – дополнение к любой архитектуре

BCE (Boundary-Control-Entity)

Один из самых ранних формализованных подходов был описан в 1992 году Иваром Якобсоном в книге «Object-Oriented Software Engineering: A Use Case Driven Approach». Он был частью анализа и проектирования ПО на основе use case'ов и позволял структурировать логику по ролям взаимодействующих компонентов.

Ключевая идея: чистая доменная логика отделена от зависимостей на внешний мир.
Entity и Control составляют доменную логику, а Boundary служит мостом между пользователем и системой. Control использует Entityдля выполнения use case, а Boundary обеспечивает ввод/вывод, превращая внешние события в вызовы Control и передавая результаты обратно пользователю или в другие системы.

Ключевые элементы:

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

    • Может содержать данные (атрибуты), операции (методы) для управления ими

    • Может содержать связи с другими сущностями (ассоциации, композиции)

    • Поведение и данные, относящиеся к одному объекту, должны быть инкапсулированы вместе

    • Должна быть создана только если есть обоснование в тексте use case

    • Не следует создавать лишние — только те, что реально нужны

    • Не зависит от пользовательского интерфейса или внешних взаимодействий

  • Control/Interactor (управляющий объект) – это объект, предназначенный для выполнения конкретного сценария использования (use case). Они получают команды от Boundary, координируют выполнение логики, взаимодействуют с Entity и управляют потоками событий в рамках одного use case.

  • Boundary/Interface object (граница) – это объект, который управляет двусторонним взаимодействием между системой и внешними акторами (пользователями или другими системами). Он преобразует действия акторов в события внутри системы и наоборот — системные события в представление, понятное актору. Для каждого типа актора создаётся свой Boundary-объект, который моделирует интерфейс взаимодействия.

Полный цикл взаимодействия в Boundary-Control-Entity
Полный цикл взаимодействия в Boundary-Control-Entity
Схема из книги И. Якобсона
Схема из книги И. Якобсона

Hexagonal Architecture (Ports & Adapters)

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

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

Ключевые элементы:

  • Application (Ядро приложения) – это внутренняя часть приложения, содержащая бизнес-логику и правила предметной области. Оно не знает ничего о механизмах ввода-вывода, фреймворках или внешних системах.

  • Ports (Порты) это интерфейсы, через которые ядро взаимодействует с внешними системами.

    • Driving/Primary (Первичные) — это основной API приложения, определяющий действия, которые внешние акторы (пользователи, UI, задачи планировщика) могут инициировать. Они реализуются первичными адаптерами и служат точками входа в бизнес-логику.
      Примеры таких портов — интерфейсы для регистрации пользователя, оформления заказа, генерации отчёта и других сценариев, запускаемых извне.

    • Driven/Secondary (Вторичные) — это интерфейсы для вторичных адаптеров. Они вызываются базовой логикой.
      Примеры таких портов — интерфейсы для хранения отдельных объектов или клиент внешнего API. Такой интерфейс просто указывает, что объект должен быть создан, извлечен, обновлен и удален. Он ничего не говорит вам о способе хранения объекта.
      Важный момент: для вторичных портов направление зависимости противоположно направлению вызова – ядро вызывает наружу, но через интерфейс, поэтому кодовая зависимость идёт от адаптера к ядру.

  • Adapters (Адаптеры) — это реализации портов для конкретных технологий. Например, адаптер для веб-интерфейса реализует первичный порт, вызывая методы ядра при поступлении HTTP-запроса, а адаптер для БД реализует вторичный порт, предоставляя данные из конкретной СУБД​

    • Driving/Primary (Первичные) — это те, кто инициирует действия (например, UI, интерфейс API, автоматический процесс). Они управляют приложением.

    • Driven/Secondary (Вторичные) – это те, которые обслуживают запросы ядра (БД, внешние сервисы, устройства). Они управляются приложением.

Hexagonal Architecture (Ports & Adapters)
Hexagonal Architecture (Ports & Adapters)

Onion Architecture

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

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

Ключевые элементы:

  • Domain model (Доменная модель) – это самый внутренний и стабильный слой. Здесь описываются объекты предметной области и бизнес-правила, независимые от каких-либо инфраструктурных деталей. Он содержит только чистую, неизменяемую суть системы.

  • Domain services (Доменные сервисы) – это слой, содержащий дополнительную бизнес-логику, не подходящую ни одной конкретной модели, а также интерфейсы зависимостей (например, репозиториев, внешних шлюзов, сервисов отправки почты и пр.). Эти интерфейсы определяются здесь, но реализуются снаружи — таким образом, домен диктует, что ему нужно, не зная как это будет реализовано.

  • Application services (Сценарии использования, координаторы) – это слой, реализующий конкретные сценарии использования приложения, оркеструя вызовы доменных моделей и обращаясь к интерфейсам, которые должны быть реализованы вне ядра. Здесь описываются варианты поведения системы: например, регистрация пользователя, оформление заказа, генерация отчёта.

  • Infrastructure и UI (Инфраструктура и пользовательский интерфейс) – это самый внешний слой, включающий реализацию интерфейсов (например, классы доступа к конкретной базе данных, реализация файлового хранилища, внешних API) и код пользовательского интерфейса (веб-контроллеры, UI-фреймворки). Этот слой зависит от всех внутренних, но внутренние не знают о нём. Он связывает приложение с реальным миром, но не влияет на внутреннюю структуру ядра.

Onion Architecture
Onion Architecture

Clean Architecture

Этот подход был сформулирован Робертом Мартином в 2012 году. Он представляет собой обобщение и развитие идей, заложенных в BCE, Hexagonal и Onion Architecture, и стремится объединить их лучшие черты.

В центре Чистой архитектуры лежит правило зависимостей (Dependency Rule):
«Зависимости в исходном коде должны быть направлены внутрь, в сторону высокоуровневых политик».

Ключевые элементы:

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

  • Use Cases (Сценарии использования) – этот слой содержит логику конкретных вариантов использования приложения. Они организуют поток данных к сущностям и от них и направляют эти сущности на использование их корпоративных бизнес-правил для достижения целей варианта использования.
    Важное свойство: изменения UI или БД не влияют на use case-слой, и наоборот, изменение логики сценария не требует менять сущности и не затрагивает внешний интерфейс (до тех пор, пока контракт не меняется).

  • Interface Adapters: Controllers, Presenters, Gateways (Интерфейсные адаптеры) – это слой, содержащий адаптеры, преобразующие данные из формата, удобного для use case и сущностей, в формат, удобный для внешних систем (БД, UI)​.

    • Controllers (обработчики HTTP-запросов)

    • Presenters/ViewModels (преобразование данных для UI)

    • Gateways/Repositories (реализации вторичных портов для доступа к данным)

  • Frameworks & Drivers: Devices, Web, UI, DB (Фреймворки и драйверы) – это самый внешний слой, обычно состоящий из фреймворков и инструментов, таких как база данных и веб-фреймворк. Как правило, для этого уровня требуется писать не очень много кода, и обычно этот код играет роль связующего звена со следующим внутренним кругом.
    На уровне фреймворков и драйверов сосредоточены все детали. Веб-интерфейс — это деталь. База данных — это деталь. Все это мы храним во внешнем круге, где они не смогут причинить большого вреда.

The Clean Architecture by Robert Martin
The Clean Architecture by Robert Martin

Адаптация, которую представил Роберт Мартин относительно BCE (Boundary-Control/Interactor-Entity) за год до написания статьи «Чистая архитектура»:

Адаптация Boundary-Control-Entity от Дяди Боба
Адаптация Boundary-Control-Entity от Дяди Боба

Сравнение подходов

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

Сравнение архитектурных подходов
Сравнение архитектурных подходов

DCI и Screaming Architecture – это дополнительные приёмы, которые помогают улучшить читаемость и понимание кода. Они не заменяют основной архитектурный каркас, а дополняют его.

Параметр

DCI (Data, Context, Interaction)

Screaming Architecture

Цель

Улучшить читаемость и понятность сценариев, разделить данные и поведение

Сделать структуру проекта понятной с первого взгляда

Основной приём

Разделение логики (ролей и сценариев) от структур данных

Группировка кода по функциональным возможностям

Где применяется

Внутри доменного слоя или слоя сценариев любой архитектуры

В организации модулей и папок проекта

Польза для всех архитектур

Упрощает реализацию и поддержку сложных сценариев, снижает путаницу в поведении объектов

Улучшает навигацию по коду, повышает согласованность и понимание структуры

Сравнение терминологии слоёв и компонентов архитектур

Общие возможные названия

  1. DOMAIN это центральный слой, содержащий бизнес-правила и сущности приложения. Он полностью независим от технических деталей.
    Альтернативы: Application Core, Business Logic, Enterprise Rules, Domain Layer

    Компоненты слоя:

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

      1. Отражают бизнес-смысл (на уровне предметной области)

      2. Инкапсулируют поведение или правила

      3. Обладают идентичностью или логической целостностью (не всегда ID, но логически целое).

      4. Не зависят от UI, БД, сетей или любых других внешних факторов.

      Альтернативы: Domain Models, Core Models, Business Objects, Business Entities, Domain Objects.

    2. Value Objects (Объект-значение)— это объекты без идентичности, которые описывают некоторую ценность, характеристику или атрибут, полностью определяемую своим содержимым (состоянием).
      Они не имеют индивидуальности — два VO с одинаковыми значениями считаются одинаковыми.

      1. Без идентичности (не имеют ID)

      2. Идентифицируются комбинацией своих атрибутов

      3. Могут использоваться внутри Entity

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

    3. Data transfer objects (Объекты передачи данных) – это структуры данных без бизнес-логики, предназначенные для транспортировки информации между слоями или компонентами системы. Часто имеют вспомогательные методы для сериализации/десериализации.

      1. Не содержат логики

      2. Легко преобразуются (json, xml)

      3. Защищают доменные модели от «утечки» наружу

      4. Используются в API, межсервисном взаимодействии, слоях UI и хранения

      Альтернативы: Schemas, Data Contracts, Transfer Objects, View Models, Models, Data

    4. Interfaces (интерфейсы) — это абстракция, которая описывает контракт взаимодействия между компонентами системы, не указывая реализацию. Они определяют, что должно быть сделано, но не как.

      1. Отделяют поведение от реализации

      2. Определяют точку взаимодействия между слоями

      3. Обеспечивают инверсию зависимостей

      4. Не содержит логики

      5. Служат контрактом, который должна реализовать инфраструктура

      Альтернативы: Ports, Boundaries, Gateways, Contracts

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

      Альтернативы: Domain Exceptions, Business Exceptions, Core Exceptions.

  2. APPLICATION - слой, координирующий доменные объекты и описывающий сценарии использования.
    Альтернативы: Use Case Layer, Control, Interactor, Application Logic, Application Core, Service Layer

    Компоненты слоя:

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

      Альтернативы: Interactors, Controllers, Application Services.

    2. Services – это классы или функции, реализующие переиспользуемую прикладную логику, которая может использоваться в нескольких use-case'ах или быть выделена для упрощения и разделения ответственности.

      Альтернативы: Use-case Services, Domain Services.

    3. Mappers – это вспомогательные компоненты, которые преобразуют Entity в DTO и обратно.

      Альтернативы: Converters, Assemblers, Transformers.

  3. INFRASTRUCTURE - слой реализации взаимодействий с внешними технологиями, базами данных, сетевыми вызовами и инфраструктурой.
    Альтернативы: Adapters, Frameworks & Drivers, External Layer, Data Access

    Компоненты слоя:

    1. Реализации репозиториев и хранилищ (Repositories, Data Access Objects (DAO), Storages, Persistence Adapters)

    2. Реализации клиентов внешних сервисов (API Clients, External Adapters, Gateway Implementations)

    3. Компоненты очередей и фоновых задач (Queue Workers, Message Brokers (Kafka, RabbitMQ, Redis), Celery Tasks)

    4. Компоненты аутентификации и авторизации (Security Adapters, OAuth Providers, JWT Implementations)

    5. Прочие адаптеры (Email/SMS Sender, Notification Services, File Storage Adapters, Caching Providers (Redis, Memcached), Logging Adapters, Search Engines (ElasticSearch))

  4. PRESENTATION - слой взаимодействия с внешним миром: пользователи, внешние системы, API.
    Альтернативы: UI, Interface Adapters, Entry Points, Delivery Layer

    Компоненты слоя:

    1. API (REST, GraphQL, gRPC) - Controllers, Endpoints, Resources, Routes, Handlers.

    2. Административный интерфейс - Admin UI, Backoffice, Control Panel, Dashboard.

    3. Представления и шаблоны - Views, Templates, Renderers.

    4. Dependencies (Wiring, DI) - Composition Root, Dependency Injection, Dependency Wiring, Bootstrapping.

    5. Background Tasks - Scheduled Jobs, Workers, Event Handlers.

General Responsibility Assignment Software Patterns (GRASP)

В данной главе я хотел кратко рассмотреть шаблоны ПО для назначения главных ответственностей, описанные Крейгом Ларманом (Craig Larman) в книге «Applying UML and Patterns».

Цель GRASP — помочь разработчикам принимать обоснованные решения о распределении обязанностей между объектами в объектно-ориентированных системах.

Перечень шаблонов:

  1. Creator (Создатель) - определяет, какой объект должен быть ответственен за создание другого объекта.
    Проблема: кто отвечает за создание нового экземпляра некоторого класса А?
    Решение: назначить классу в обязанность создавать экземпляры класса А, если выполняется одно (или несколько) из следующих условий.

    1. Класс В содержит (contains) или агрегирует (aggregate) объекты А.

    2. Класс B записывает (records) экземпляры объектов А.

    3. Класс B активно использует (closely uses) объекты А.

    4. Класс B обладает данными инициализации (has the initializing data) для объектов А.

    Пример: если Order агрегирует ProductItem, то он должен создавать ProductItem.

  2. Information Expert (Информационный эксперт) - назначай ответственность тому объекту, который имеет необходимую информацию для её выполнения.
    Проблема: каков базовый принцип распределения обязанностей между объектами?
    Решение: назначить эту обязанность тому классу, который обладает достаточной информацией для ее выполнения.
    Пример: если Order знает о своих ProductItem, он должен рассчитывать total_price.

  3. Low Coupling (Низкая связанность / низкое связывание / низкая внешняя зависимость) - объекты должны быть как можно менее зависимыми друг от друга.
    Снижается влияние изменений и повышается переиспользуемость.
    Проблема: как уменьшить влияние вносимых изменений на другие объекты?
    Решение: минимизировать степень связанности в процессе распределения обязанностей. Этот принцип используется для оценки различных альтернатив.

  4. High Cohesion (Высокая связность / высокое зацепление / высокая внутренняя согласованность) - объекты должны быть фокусированы на одной задаче, делать только то, что они должны. Повышает читаемость и поддерживаемость.
    Проблема: как обеспечить сфокусированность обязанностей объектов, их управляемость и ясность, а заодно выполнение принципа Low Coupling?
    Решение: обеспечивать высокий уровень внутренней согласованности в процессе распределения обязанностей. Этот принцип нужно использовать для оценки различных альтернатив.

  5. Controller - вводит посредника (контроллер), который обрабатывает внешние запросы и делегирует работу другим объектам.
    Проблема: кто должен отвечать за получение и координацию выполнения системных операций, поступающих от уровня интерфейса пользователя?
    Решение: присвоить эту обязанность классу, удовлетворяющему одному из следующих условий.

    1. Класс представляет всю систему в целом, корневой объект, устройство или важную подсистему (внешний контроллер).

    2. Класс представляет сценарий некоторого прецедента, в рамках которого выполняется обработка этой системной операции (контроллер прецедента или контроллер сеанса).

    Пример: Web-контроллер, обрабатывающий запросы к бизнес-логике.

  6. Polymorphism (Полиморфизм) - Используй полиморфизм для делегирования поведения объектам, зависящим от типа.
    Проблема: как обрабатывать альтернативные варианты поведения на основе типа? Как создавать подключаемые программные компоненты?
    Решение: если поведение объектов одного типа (класса) может изменяться, обязанности распределяются для различных вариантов поведения с использованием полиморфных операций для этого класса.
    Пример: интерфейс NotificationSender, с реализациями EmailSender, SMSSender.

  7. Pure Fabrication (Чистая синтетика) - создай искусственный (недоменный) класс, если это улучшает низкую связанность или высокую связность.
    Проблема: какой класс должен обеспечить реализацию шаблонов High Cohesion и Low Coupling или других принципов проектирования, если шаблон Ехрert (например) не обеспечивает подходящего решения?
    Объектно-ориентированные системы отличаются тем, что программные классы реализуют понятия предметной области, как, например, классы Sale и Customer. Однако существует множество ситуаций, когда распределение обязанностей только между такими классами приводит к проблемам со связностью и связанностью, т.е. с невозможностью повторного использования кода.
    Решение: присвоить группу обязанностей с высокой степенью внутренней согласованности искусственному классу, не представляющему конкретного понятия предметной области, т.е. синтезировать искусственную сущность для поддержки высокого зацепления, слабого связывания и повторного использования.
    Такой класс является продуктом нашего воображения и представляет собой синтетику (fabrication). В идеале присвоенные этому классу обязанности поддерживают высокую степень связности и низкой связанности, так что структура этого синтетического класса является очень прозрачной, или чистой (pure).
    Пример: реализовать PersistentStorage, который не является доменной моделью, для манипуляций с данными доменной модели Sale.

  8. Indirection (Косвенность / Посредник / Перенаправление) - вводи посредника между двумя компонентами, чтобы снизить их прямую связанность.
    Проблема: как распределить обязанности, чтобы обеспечить отсутствие прямой связанности; снизить уровень связанности объектов, согласно шаблону Low Coupling, и сохранить высокий потенциал повторного использования?
    Решение: присвоить обязанности промежуточному объекту для обеспечения связи между другими компонентами или службами, которые не связаны между собой напрямую.
    Пример: подойдёт пример из Pure Fabrication, т.е. класс РеrsistentStorage выступает в роли промежуточного звена между классом Sale и базой данных.

  9. Protected Variations (Защищённые изменения) - защищай от изменений, вводя стабильные интерфейсы между изменчивыми компонентами.
    Проблема: как спроектировать объекты, подсистемы и систему, чтобы изменение этих элементов не оказывало нежелательного влияния на другие элементы?
    Решение: идентифицировать точки возможных вариаций или неустойчивости; распределить обязанности таким образом, чтобы обеспечить устойчивый интерфейс.
    Пример: интерфейс IPaymentGateway, скрывающий Stripe/PayPal/ЮKassa реализацию.

Заключение

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

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

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

Источники

  • Bernd Bruegge & Allen H. Dutoit «Object-Oriented Software Engineering: Conquering Complex and Changing Systems»

  • Ivar Jacobson «Object-Oriented Software Engineering: A Use Case Driven Approach»

  • Robert C. Martin «Clean Architecture: A Craftsman's Guide to Software Structure and Design»

  • Craig Larman «APPLYING UML AND PATTERNS: An Introduction to Object-Oriented analysis and Design and Iterative Development»

  • https://blog.cleancoder.com/

  • https://jeffreypalermo.com/

Теги:
Хабы:
+8
Комментарии19

Публикации

Работа

Ближайшие события