Предисловие
Цель этой статьи — объединить и кратко изложить все базовые архитектурные подходы: их терминологию, концепции и отличительные черты. Собрать всё воедино, чтобы можно было относительно быстро вникнуть в основы.
Я решил написать серию статей, посвящённых различным аспектам проектирования программных систем, но первоначальной идеей было показать архитектурное решение моего 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. Пользовательский интерфейс можно легко изменять, не затрагивая остальную систему. Например, веб-интерфейс можно заменить консольным интерфейсом, не изменяя бизнес-правил.
-
Независимость от БД. Можно сменить СУБД или вообще вместо БД использовать, к примеру, файлы – бизнес-логика не зависит от деталей хранения.
-
Независимость от внешних агентов. Бизнес-правила ничего не знают о внешнем мире и окружении.
Историческая сводка:
-
BCE (Boundary, Control, Entity) (Граница, Управление, Сущность) - 1992 г.
-
Hexagonal Architecture (Гексагональная архитектура, «Порты и адаптеры») – 2005 г.
-
Onion Architecture (Луковичная архитектура) – 2008 г.
-
Clean Architecture (Чистая архитектура) – 2012 г.
-
DCI (Data, Context, Interaction) (Данные, Контекст и Взаимодействие) – ~2009 г. – дополнение к любой архитектуре
-
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-объект, который моделирует интерфейс взаимодействия.


Hexagonal Architecture (Ports & Adapters)
Архитектурный подход, предложенный Алистером Кокберном в 2005 году, преследует ту же цель – отделить бизнес-логику от внешних деталей. В гексагональной архитектуре вместо привычных горизонтальных слоев используется метафора внутреннего шестигранника и окружающих его сторон.
Ключевая идея: архитектура не о слоях, а о границах и точках взаимодействия. Порты определяют, что нужно системе, а адаптеры реализуют как это достигается.
Ключевые элементы:
-
Application (Ядро приложения) – это внутренняя часть приложения, содержащая бизнес-логику и правила предметной области. Оно не знает ничего о механизмах ввода-вывода, фреймворках или внешних системах.
-
Ports (Порты) – это интерфейсы, через которые ядро взаимодействует с внешними системами.
-
Driving/Primary (Первичные) — это основной API приложения, определяющий действия, которые внешние акторы (пользователи, UI, задачи планировщика) могут инициировать. Они реализуются первичными адаптерами и служат точками входа в бизнес-логику.
Примеры таких портов — интерфейсы для регистрации пользователя, оформления заказа, генерации отчёта и других сценариев, запускаемых извне. -
Driven/Secondary (Вторичные) — это интерфейсы для вторичных адаптеров. Они вызываются базовой логикой.
Примеры таких портов — интерфейсы для хранения отдельных объектов или клиент внешнего API. Такой интерфейс просто указывает, что объект должен быть создан, извлечен, обновлен и удален. Он ничего не говорит вам о способе хранения объекта.
Важный момент: для вторичных портов направление зависимости противоположно направлению вызова – ядро вызывает наружу, но через интерфейс, поэтому кодовая зависимость идёт от адаптера к ядру.
-
-
Adapters (Адаптеры) — это реализации портов для конкретных технологий. Например, адаптер для веб-интерфейса реализует первичный порт, вызывая методы ядра при поступлении HTTP-запроса, а адаптер для БД реализует вторичный порт, предоставляя данные из конкретной СУБД
-
Driving/Primary (Первичные) — это те, кто инициирует действия (например, UI, интерфейс API, автоматический процесс). Они управляют приложением.
-
Driven/Secondary (Вторичные) – это те, которые обслуживают запросы ядра (БД, внешние сервисы, устройства). Они управляются приложением.
-

Onion Architecture
Архитектура, предложенная Джеффри Палермо в 2008 году. Основная идея остаётся всё той же - сосредоточиться на модели предметной области, минимизируя зависимости от инфраструктуры. Она развивает идеи гексагональной архитектуры и традиционного многослойного подхода, делая акцент на инверсии зависимостей и максимальной защите бизнес-логики от внешних изменений.
Ключевая идея: все зависимости направлены к центру. Система представляется в виде концентрических слоёв («колец луковицы»). Самый внутренний слой – доменные модели и бизнес-правила – не зависит ни от каких внешних слоёв. Внешние же слои могут зависеть только от лежащих глубже (более центральных) слоёв, но не наоборот. Это достигается за счёт использования интерфейсов: внутренние слои определяют абстракции, а внешние слои их реализуют.
Ключевые элементы:
-
Domain model (Доменная модель) – это самый внутренний и стабильный слой. Здесь описываются объекты предметной области и бизнес-правила, независимые от каких-либо инфраструктурных деталей. Он содержит только чистую, неизменяемую суть системы.
-
Domain services (Доменные сервисы) – это слой, содержащий дополнительную бизнес-логику, не подходящую ни одной конкретной модели, а также интерфейсы зависимостей (например, репозиториев, внешних шлюзов, сервисов отправки почты и пр.). Эти интерфейсы определяются здесь, но реализуются снаружи — таким образом, домен диктует, что ему нужно, не зная как это будет реализовано.
-
Application services (Сценарии использования, координаторы) – это слой, реализующий конкретные сценарии использования приложения, оркеструя вызовы доменных моделей и обращаясь к интерфейсам, которые должны быть реализованы вне ядра. Здесь описываются варианты поведения системы: например, регистрация пользователя, оформление заказа, генерация отчёта.
-
Infrastructure и UI (Инфраструктура и пользовательский интерфейс) – это самый внешний слой, включающий реализацию интерфейсов (например, классы доступа к конкретной базе данных, реализация файлового хранилища, внешних API) и код пользовательского интерфейса (веб-контроллеры, UI-фреймворки). Этот слой зависит от всех внутренних, но внутренние не знают о нём. Он связывает приложение с реальным миром, но не влияет на внутреннюю структуру ядра.

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 (Фреймворки и драйверы) – это самый внешний слой, обычно состоящий из фреймворков и инструментов, таких как база данных и веб-фреймворк. Как правило, для этого уровня требуется писать не очень много кода, и обычно этот код играет роль связующего звена со следующим внутренним кругом.
На уровне фреймворков и драйверов сосредоточены все детали. Веб-интерфейс — это деталь. База данных — это деталь. Все это мы храним во внешнем круге, где они не смогут причинить большого вреда.

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

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

DCI и Screaming Architecture – это дополнительные приёмы, которые помогают улучшить читаемость и понимание кода. Они не заменяют основной архитектурный каркас, а дополняют его.
Параметр |
DCI (Data, Context, Interaction) |
Screaming Architecture |
Цель |
Улучшить читаемость и понятность сценариев, разделить данные и поведение |
Сделать структуру проекта понятной с первого взгляда |
Основной приём |
Разделение логики (ролей и сценариев) от структур данных |
Группировка кода по функциональным возможностям |
Где применяется |
Внутри доменного слоя или слоя сценариев любой архитектуры |
В организации модулей и папок проекта |
Польза для всех архитектур |
Упрощает реализацию и поддержку сложных сценариев, снижает путаницу в поведении объектов |
Улучшает навигацию по коду, повышает согласованность и понимание структуры |
Сравнение терминологии слоёв и компонентов архитектур
Общие возможные названия
-
DOMAIN – это центральный слой, содержащий бизнес-правила и сущности приложения. Он полностью независим от технических деталей.
Альтернативы: Application Core, Business Logic, Enterprise Rules, Domain LayerКомпоненты слоя:
-
Entities (сущности) - это бизнес-объекты, которые инкапсулируют важную информацию предметной области и поведение, связанное с этой информацией. Они могут быть долгоживущими, но не обязаны — главное, чтобы они отражали значимые концепты, а не были просто транспортом данных.
-
Отражают бизнес-смысл (на уровне предметной области)
-
Инкапсулируют поведение или правила
-
Обладают идентичностью или логической целостностью (не всегда ID, но логически целое).
-
Не зависят от UI, БД, сетей или любых других внешних факторов.
Альтернативы: Domain Models, Core Models, Business Objects, Business Entities, Domain Objects.
-
-
Value Objects (Объект-значение)— это объекты без идентичности, которые описывают некоторую ценность, характеристику или атрибут, полностью определяемую своим содержимым (состоянием).
Они не имеют индивидуальности — два VO с одинаковыми значениями считаются одинаковыми.-
Без идентичности (не имеют ID)
-
Идентифицируются комбинацией своих атрибутов
-
Могут использоваться внутри Entity
-
Используются для представления измеримых, вычисляемых или структурных характеристик
-
-
Data transfer objects (Объекты передачи данных) – это структуры данных без бизнес-логики, предназначенные для транспортировки информации между слоями или компонентами системы. Часто имеют вспомогательные методы для сериализации/десериализации.
-
Не содержат логики
-
Легко преобразуются (json, xml)
-
Защищают доменные модели от «утечки» наружу
-
Используются в API, межсервисном взаимодействии, слоях UI и хранения
Альтернативы: Schemas, Data Contracts, Transfer Objects, View Models, Models, Data
-
-
Interfaces (интерфейсы) — это абстракция, которая описывает контракт взаимодействия между компонентами системы, не указывая реализацию. Они определяют, что должно быть сделано, но не как.
-
Отделяют поведение от реализации
-
Определяют точку взаимодействия между слоями
-
Обеспечивают инверсию зависимостей
-
Не содержит логики
-
Служат контрактом, который должна реализовать инфраструктура
Альтернативы: Ports, Boundaries, Gateways, Contracts
-
-
Exceptions - специфичные ошибки предметной области. В контексте архитектурных подходов, исключения служат для сигнализации о нарушении бизнес-правил, ошибках взаимодействия с инфраструктурой или некорректном вводе.
Альтернативы: Domain Exceptions, Business Exceptions, Core Exceptions.
-
-
APPLICATION - слой, координирующий доменные объекты и описывающий сценарии использования.
Альтернативы: Use Case Layer, Control, Interactor, Application Logic, Application Core, Service LayerКомпоненты слоя:
-
Use Cases – это специфичная для конкретного приложения и его пользовательских сценариев логика, описывающая как система должна вести себя в ответ на внешние действия. Они реализуют конкретное поведение, координируя работу доменных сущностей, интерфейсов и сервисов.
Альтернативы: Interactors, Controllers, Application Services.
-
Services – это классы или функции, реализующие переиспользуемую прикладную логику, которая может использоваться в нескольких use-case'ах или быть выделена для упрощения и разделения ответственности.
Альтернативы: Use-case Services, Domain Services.
-
Mappers – это вспомогательные компоненты, которые преобразуют Entity в DTO и обратно.
Альтернативы: Converters, Assemblers, Transformers.
-
-
INFRASTRUCTURE - слой реализации взаимодействий с внешними технологиями, базами данных, сетевыми вызовами и инфраструктурой.
Альтернативы: Adapters, Frameworks & Drivers, External Layer, Data AccessКомпоненты слоя:
-
Реализации репозиториев и хранилищ (Repositories, Data Access Objects (DAO), Storages, Persistence Adapters)
-
Реализации клиентов внешних сервисов (API Clients, External Adapters, Gateway Implementations)
-
Компоненты очередей и фоновых задач (Queue Workers, Message Brokers (Kafka, RabbitMQ, Redis), Celery Tasks)
-
Компоненты аутентификации и авторизации (Security Adapters, OAuth Providers, JWT Implementations)
-
Прочие адаптеры (Email/SMS Sender, Notification Services, File Storage Adapters, Caching Providers (Redis, Memcached), Logging Adapters, Search Engines (ElasticSearch))
-
-
PRESENTATION - слой взаимодействия с внешним миром: пользователи, внешние системы, API.
Альтернативы: UI, Interface Adapters, Entry Points, Delivery LayerКомпоненты слоя:
-
API (REST, GraphQL, gRPC) - Controllers, Endpoints, Resources, Routes, Handlers.
-
Административный интерфейс - Admin UI, Backoffice, Control Panel, Dashboard.
-
Представления и шаблоны - Views, Templates, Renderers.
-
Dependencies (Wiring, DI) - Composition Root, Dependency Injection, Dependency Wiring, Bootstrapping.
-
Background Tasks - Scheduled Jobs, Workers, Event Handlers.
-
General Responsibility Assignment Software Patterns (GRASP)
В данной главе я хотел кратко рассмотреть шаблоны ПО для назначения главных ответственностей, описанные Крейгом Ларманом (Craig Larman) в книге «Applying UML and Patterns».
Цель GRASP — помочь разработчикам принимать обоснованные решения о распределении обязанностей между объектами в объектно-ориентированных системах.
Перечень шаблонов:
-
Creator (Создатель) - определяет, какой объект должен быть ответственен за создание другого объекта.
Проблема: кто отвечает за создание нового экземпляра некоторого класса А?
Решение: назначить классу в обязанность создавать экземпляры класса А, если выполняется одно (или несколько) из следующих условий.-
Класс В содержит (contains) или агрегирует (aggregate) объекты А.
-
Класс B записывает (records) экземпляры объектов А.
-
Класс B активно использует (closely uses) объекты А.
-
Класс B обладает данными инициализации (has the initializing data) для объектов А.
Пример: если
Order
агрегируетProductItem
, то он должен создаватьProductItem
. -
-
Information Expert (Информационный эксперт) - назначай ответственность тому объекту, который имеет необходимую информацию для её выполнения.
Проблема: каков базовый принцип распределения обязанностей между объектами?
Решение: назначить эту обязанность тому классу, который обладает достаточной информацией для ее выполнения.
Пример: еслиOrder
знает о своихProductItem
, он должен рассчитыватьtotal_price
. -
Low Coupling (Низкая связанность / низкое связывание / низкая внешняя зависимость) - объекты должны быть как можно менее зависимыми друг от друга.
Снижается влияние изменений и повышается переиспользуемость.
Проблема: как уменьшить влияние вносимых изменений на другие объекты?
Решение: минимизировать степень связанности в процессе распределения обязанностей. Этот принцип используется для оценки различных альтернатив. -
High Cohesion (Высокая связность / высокое зацепление / высокая внутренняя согласованность) - объекты должны быть фокусированы на одной задаче, делать только то, что они должны. Повышает читаемость и поддерживаемость.
Проблема: как обеспечить сфокусированность обязанностей объектов, их управляемость и ясность, а заодно выполнение принципа Low Coupling?
Решение: обеспечивать высокий уровень внутренней согласованности в процессе распределения обязанностей. Этот принцип нужно использовать для оценки различных альтернатив. -
Controller - вводит посредника (контроллер), который обрабатывает внешние запросы и делегирует работу другим объектам.
Проблема: кто должен отвечать за получение и координацию выполнения системных операций, поступающих от уровня интерфейса пользователя?
Решение: присвоить эту обязанность классу, удовлетворяющему одному из следующих условий.-
Класс представляет всю систему в целом, корневой объект, устройство или важную подсистему (внешний контроллер).
-
Класс представляет сценарий некоторого прецедента, в рамках которого выполняется обработка этой системной операции (контроллер прецедента или контроллер сеанса).
Пример: Web-контроллер, обрабатывающий запросы к бизнес-логике.
-
-
Polymorphism (Полиморфизм) - Используй полиморфизм для делегирования поведения объектам, зависящим от типа.
Проблема: как обрабатывать альтернативные варианты поведения на основе типа? Как создавать подключаемые программные компоненты?
Решение: если поведение объектов одного типа (класса) может изменяться, обязанности распределяются для различных вариантов поведения с использованием полиморфных операций для этого класса.
Пример: интерфейсNotificationSender
, с реализациямиEmailSender
,SMSSender
. -
Pure Fabrication (Чистая синтетика) - создай искусственный (недоменный) класс, если это улучшает низкую связанность или высокую связность.
Проблема: какой класс должен обеспечить реализацию шаблонов High Cohesion и Low Coupling или других принципов проектирования, если шаблон Ехрert (например) не обеспечивает подходящего решения?
Объектно-ориентированные системы отличаются тем, что программные классы реализуют понятия предметной области, как, например, классы Sale и Customer. Однако существует множество ситуаций, когда распределение обязанностей только между такими классами приводит к проблемам со связностью и связанностью, т.е. с невозможностью повторного использования кода.
Решение: присвоить группу обязанностей с высокой степенью внутренней согласованности искусственному классу, не представляющему конкретного понятия предметной области, т.е. синтезировать искусственную сущность для поддержки высокого зацепления, слабого связывания и повторного использования.
Такой класс является продуктом нашего воображения и представляет собой синтетику (fabrication). В идеале присвоенные этому классу обязанности поддерживают высокую степень связности и низкой связанности, так что структура этого синтетического класса является очень прозрачной, или чистой (pure).
Пример: реализоватьPersistentStorage
, который не является доменной моделью, для манипуляций с данными доменной моделиSale
. -
Indirection (Косвенность / Посредник / Перенаправление) - вводи посредника между двумя компонентами, чтобы снизить их прямую связанность.
Проблема: как распределить обязанности, чтобы обеспечить отсутствие прямой связанности; снизить уровень связанности объектов, согласно шаблону Low Coupling, и сохранить высокий потенциал повторного использования?
Решение: присвоить обязанности промежуточному объекту для обеспечения связи между другими компонентами или службами, которые не связаны между собой напрямую.
Пример: подойдёт пример из Pure Fabrication, т.е. классРеrsistentStorage
выступает в роли промежуточного звена между классом Sale и базой данных. -
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»
Автор: mpanaryin