Слоистая архитектура (Layered / N-tier)

Общее

Слоистая архитектура (layered architecture; в корпоративной традиции — N-tier, «N-звенная») — архитектурный стиль, при котором система разделяется на горизонтальные слои, каждый из которых выполняет строго одну роль: представление данных, координацию сценариев, бизнес-правила, доступ к данным. Слой обслуживает запросы слоя, расположенного выше, и сам пользуется услугами слоя, расположенного ниже; всё, что не входит в роль слоя, в его коде не пишется. Идея восходит к разделению ответственности (separation of concerns) и остаётся самым старым, самым известным и самым распространённым способом структурирования корпоративного ПО: от настольных приложений на Delphi и Visual Basic до современных бэкендов на Spring, ASP.NET и Django.

Историческая линия стиля — одна из самых длинных в архитектуре ПО. Эдсгер Дейкстра в статье о мультизадачной системе THE (The Structure of the «THE»-Multiprogramming System, 1968) показал, что сложность системы укрощается иерархией уровней: каждый уровень опирается только на нижележащие и скрывает их устройство от вышележащих; там же он ввёл сам термин «разделение ответственности». В 1970-е идея превратилась в метод: Дэвид Парнас (статья On the Criteria To Be Used in Decomposing Systems into Modules, 1972) обосновал информационную закрытость — модуль скрывает проектное решение, а не просто группирует функции, — а Эдвард Йордан и Ларри Константайн в книге Structured Design (1979) дали количественные ориентиры: высокая связность внутри модулей, низкое сцепление между ними. Горизонтальные слои оказались простейшим способом добиться обоих свойств сразу.

Корпоративная волна превратила идею в индустриальный стандарт. В 1960–70-е системы были однозвенными: терминал, логика и данные жили в одном мэйнфрейме. В 1980–90-е пришёл клиент-сервер: двухзвенная схема «толстый клиент + СУБД» с бизнес-логикой в клиенте или в хранимых процедурах. На рубеже 1990-х выделился третий уровень — сервер приложений (CORBA, DCOM, MTS/COM+, J2EE/EJB, позднее .NET), и маркетинг вендоров закрепил термин N-tier: приложение состоит из N физически раздельных звеньев. Веб-эпоха сделала эту топологию нормой (браузер — веб-сервер — сервер приложений — СУБД), а Мартин Фаулер в книге Patterns of Enterprise Application Architecture (PoEAA, 2002) каталогизировал слоистые паттерны кода — от Transaction Script до Data Mapper — и зафиксировал рабочее определение слоя.

ГодВехаЗначение для стиля
1968Дейкстра, система THEИерархия уровней; термин separation of concerns
1972Парнас, декомпозиция модулейИнформационная закрытость: слой скрывает проектное решение
1979Йордан и Константайн, Structured DesignСвязность и сцепление как критерии разбиения
1990-еСерверы приложений (CORBA, DCOM, EJB)3-tier как продукт; термин N-tier
2002Фаулер, PoEAAКаталог слоистых паттернов; определение слоя
2005–2012Hexagonal, Onion, Clean ArchitectureПереосмысление правила зависимостей («внутрь» вместо «вниз»)

В этой эволюции слоистая архитектура занимает место точки отсчёта: это базовый стиль, из которого выросли все последующие уточнения. Обратите внимание на последнюю строку таблицы: гексагональная архитектура Алистера Коубёрна, «луковица» Джеффри Палермо и Clean Architecture Роберта Мартина не отменили слои — они поменяли правило направления зависимостей. Подробно этот поворот разобран в конце статьи и в материале о Clean Architecture; здесь же слоистая архитектура рассматривается в её классической форме.

Терминологическая оговорка, снимающая половину споров. Слой (layer) — логическое деление: участок кода со своей ролью, живущий в общем процессе и вызываемый обычными вызовами. Звено (tier) — физическое деление: узел развёртывания, со своим процессом, машиной и сетевым соединением. Трёхслойное приложение может разворачиваться одним звеном (весь код на одном сервере), двумя (клиент и сервер) или четырьмя (браузер — веб-сервер — сервер приложений — СУБД). Слои описывают организацию кода, звенья — топологию развёртывания; N-tier — это слоистая архитектура плюс решение вынести часть слоёв на отдельные машины.

В литературе и вакансиях стиль встречается под синонимами: multi-tier architecture, n-layer architecture, «трёхзвенная архитектура», «слоеная архитектура». Иногда термином «слоистая архитектура» обозначают только логическое деление, а N-tier — только физическое; в этой статье оба аспекта рассматриваются вместе, поскольку на практике они неразделимы: решение «сколько звеньев» всегда следует из решения «сколько слоёв и какие».

Почему деление именно горизонтальное — и почему оно работает? Слой — это уровень абстракции: каждый следующий слой говорит на языке, более близком к человеку, и скрывает язык нижнего. Presentation объясняется терминами пользователя и протоколов, Application — намерениями («оформить заказ»), Domain — понятиями бизнеса, Data Access — таблицами и SQL. Поднимаясь по стопке, разработчик переходит от техники к смыслу; спускаясь — от смысла к железу. Это и есть практическое воплощение разделения ответственности, введённого во введении в архитектуру: каждая проблема системы получает свой язык и своё место.

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

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

┌──────────────────────────────────────────────────────────┐
│               Presentation (представление)               │
│   UI, HTTP-контроллеры, REST/gRPC, сериализация, DTO     │
└────────────────────────────┬─────────────────────────────┘
                             │ вызывает / зависит от
┌────────────────────────────▼─────────────────────────────┐
│                 Application (приложение)                 │
│   сервисы сценариев, оркестрация, границы транзакций     │
└────────────────────────────┬─────────────────────────────┘
                             │ вызывает / зависит от
┌────────────────────────────▼─────────────────────────────┐
│              Domain (домен, бизнес-логика)               │
│   сущности, бизнес-правила, инварианты, расчёты          │
└────────────────────────────┬─────────────────────────────┘
                             │ вызывает / зависит от
┌────────────────────────────▼─────────────────────────────┐
│              Data Access (доступ к данным)               │
│   репозитории, ORM, SQL-запросы, клиенты внешних API     │
└────────────────────────────┴─────────────────────────────┘

        Направление зависимостей — строго сверху вниз

Роли слоёв:

СлойОтветственностьТипичные представителиЧего в нём не бывает
PresentationПринять ввод из внешнего мира, вернуть результатUI, HTTP-контроллеры, view models, REST/gRPC-эндпоинты, сериализацияБизнес-правила, SQL
ApplicationВыполнить сценарий: оркестрация, координация, границы транзакцийСервисы приложения, use case-классы, обработчики командБизнес-правила, привязка к протоколу
DomainБизнес-логика: правила и инварианты предприятияСущности, объекты-значения, расчёты, конечные автоматыHTTP, SQL, фреймворки, сериализация
Data AccessСохранить и извлечь данные, скрыть способ храненияРепозитории, ORM, SQL, клиенты внешних сервисовБизнес-решения

Классическое трёхслойное деление (presentation — business — data) в этой таблице уже расщеплено надвое: бизнес-слой разделён на Application и Domain. Это уточнение пришло из DDD и PoEAA и сегодня считается полезным по умолчанию: сценарная координация и бизнес-правила меняются по разным причинам, и смешение их в одном «бизнес-слое» — источник большинства деградаций слоистого кода.

Правило зависимостей классической слоистой архитектуры: зависимости направлены строго сверху вниз. Слой знает о слое, расположенном непосредственно под ним, и обращается только к его интерфейсу; о слоях выше он не знает ничего. Presentation вызывает Application, Application пользуется Domain и через него — Data Access; нижние слои никогда не вызывают верхние напрямую. Вместе с правилом работает изоляция слоёв: каждый слой скрывает своё устройство от соседей — Presentation не знает, что под ним ORM, а не SQL-запросы; Data Access не знает, что его вызывает REST-контроллер, а не консольная команда. Фаулер формулирует признаки здорового слоя так: его можно заменить переписанной реализацией с тем же интерфейсом, не тронув соседей.

Граница между слоями — это всегда явный контракт: набор интерфейсов, которые нижний слой предъявляет верхнему. Именно контракт, а не папка, делает слой заменяемым: пока сигнатуры и семантика (включая гарантии транзакций и ошибок) сохраняются, реализация под ними может быть переписана целиком. Отсюда практическое правило ведения слоистой кодовой базы: публичная поверхность слоя — короткий список стабильных интерфейсов, всё остальное — внутренности, недоступные соседям. В классической традиции контракт доступа к данным (например, IOrderRepository) объявляется на стыке Application и Data Access и реализуется нижним слоем — зависимость при этом по-прежнему идёт вниз.

В стопке действует и собственный градиент изменчивости: верхние слои меняются чаще нижних — UI-фреймворки выходят раз в пару лет, интерфейсы — чаще, СУБД переживает десятилетия. Но заметим: этот градиент перевёрнут относительно ценности кода. Самое ценное — бизнес-правила — зажато в середине стопки между двумя изменчивыми мирами и при этом зависит от нижнего, инфраструктурного. Это внутреннее противоречие классического стиля; его осознание и стало позже толчком к пересмотру правила зависимостей (раздел «Слоистая vs Clean / Hexagonal»).

Практическое следствие изоляции — несколько доставок на одну логику. Поскольку Domain и Application не знают о способе ввода-вывода, один и тот же код обслуживает веб-интерфейс, публичный API, консольные команды и обработчик очереди сообщений: для каждой доставки пишется лишь свой тонкий слой Presentation. Это старейшая и самая окупающаяся выгода слоистого разбиения.

Уточнение о закрытых и открытых слоях, которое в явном виде сформулировал Марк Ричардс (Software Architecture Patterns, 2015). Закрытый слой обязан участвовать в каждом запросе: вызов с верхнего уровня проходит через все нижележащие уровни, даже если ему там нечего делать. Закрытость даёт изоляцию (слои можно менять независимо), но порождает «каскад» — простой запрос чтения вынужден пройти три слоя. Открытый слой допускает вызов «через себя»: типичный пример — сервисный слой, помеченный как открытый, чтобы инфраструктурные операции (аудит, почта) могли вызывать Data Access напрямую, минуя сценарии. Число открытых слоёв — компромисс между чистотой изоляции и здравым смыслом; полностью открытая стопка вырождается в набор беззащитных пакетов.

Проследим путь типичного запроса «оформить заказ» через стопку:

  1. Presentation. HTTP-контроллер принимает POST /orders, проверяет транспортную корректность, собирает DTO и вызывает метод сервиса приложения.
  2. Application. Сервис сценария OrderService.placeOrder(...) открывает границу транзакции, загружает нужные объекты через репозитории, поручает им работу и фиксирует результат.
  3. Domain. Сущность Order проверяет бизнес-правила — лимит кредита, доступность товаров, расчёт скидки — и либо принимает новое состояние, либо отклоняет операцию.
  4. Data Access. Репозиторий сохраняет агрегат через ORM (или SQL), скрывая от верхних слоёв и диалект СУБД, и само существование ORM.
  5. Результат возвращается обратно вверх: сервис завершает транзакцию, контроллер сериализует ответ, клиент получает JSON.

В коде слоистая система узнаётся по структуре проекта — это одновременно и самый тиражируемый, и самый ругаемый её атрибут:

src/
├── Presentation/          — контроллеры, фильтры, view models, сериализация
├── Application/           — сервисы сценариев, DTO, границы транзакций
├── Domain/                — сущности, объекты-значения, бизнес-правила
└── Infrastructure/        — репозитории, ORM-маппинги, клиенты внешних API

Важно понимать, что сама по себе структура папок правила зависимостей не обеспечивает. Если Domain импортирует типы ORM, а контроллер дергает репозиторий напрямую — стопка остаётся косметикой. Как и в модульном монолите, направление зависимостей стоит контролировать автоматически: ArchUnit (Java), NetArchTest (.NET), dependency-cruiser (JavaScript) проверяют импорты и ломают сборку при нарушении.

Та же система в коде (C# — как и в примерах статьи о Clean Architecture, для наглядного контраста с ней). Обратите внимание, где живут интерфейсы и где — бизнес-правила:

// ── Presentation ──────────────────────────────────────────────

// Контроллер: переводит HTTP в вызов сценария, сериализует ответ.
// Бизнес-решений не принимает.
[HttpPost("/orders")]
public IActionResult PlaceOrder([FromBody] PlaceOrderDto dto)
{
    var orderId = _orderService.PlaceOrder(dto.CustomerId, dto.ItemIds);
    return Ok(new { orderId });
}

// ── Application ───────────────────────────────────────────────

public class OrderService
{
    private readonly IOrderRepository _repository;

    public OrderService(IOrderRepository repository) => _repository = repository;

    public int PlaceOrder(int customerId, IReadOnlyList<int> itemIds)
    {
        var customer = _repository.FindCustomer(customerId);
        var order = customer.StartOrder();               // бизнес-правило — в домене

        foreach (var itemId in itemIds)
            order.AddItem(_repository.FindItem(itemId)); // и это тоже

        order.Place();                                   // проверка инвариантов — в домене
        _repository.Save(order);                         // persistence-решение — здесь
        return order.Id;
    }
}

// ── Domain ────────────────────────────────────────────────────

public class Order
{
    private readonly List<OrderItem> _items = new();

    public OrderStatus Status { get; private set; }
    public IReadOnlyList<OrderItem> Items => _items;

    public void AddItem(Product item)
    {
        if (Status != OrderStatus.Draft)
            throw new DomainException("Only draft orders can be modified");
        _items.Add(new OrderItem(item, item.Price));
    }

    public void Place()
    {
        if (_items.Count == 0)
            throw new DomainException("Order must contain items");
        Status = OrderStatus.Placed;
    }
}

// ── Data Access ───────────────────────────────────────────────

public class SqlOrderRepository : IOrderRepository
{
    private readonly AppDbContext _db;   // ORM, соединения, SQL — только здесь

    public Customer FindCustomer(int id) => _db.Customers.Find(id);
    public Product FindItem(int id) => _db.Products.Find(id);
    public void Save(Order order) => _db.Orders.Add(order);
}

Пример демонстрирует здоровую классическую стопку: контроллер не знает о репозитории, сущность не знает об ORM, ORM не знает о HTTP. Интерфейс IOrderRepository в классической традиции объявляется в нижней части стопки (в Domain или Application — оба варианта встречаются) и реализуется в Data Access — зависимость идёт вниз. Чего пример пока не показывает: как тестировать OrderService без базы и что делать, когда домен станет богаче, а зависимость от ORM — теснее. Это предмет следующего раздела и раздела о сравнении с Clean Architecture.

Экспресс-чеклист здоровья слоистой кодовой базы — пять вопросов для ревью и аудита:

  • Где живёт бизнес-правило X? Здоровый ответ называет один слой; если для ответа нужно перечислить SQL, сервис и контроллер — домен протёк.
  • Что придётся изменить, чтобы добавить вторую доставку (REST → плюс консольная команда плюс очередь)? В здоровой стопке — только новый участок Presentation.
  • Импортирует ли Domain что-либо технологическое — ORM, драйверы, HTTP-типы, сериализацию? Любой такой импорт — прямое нарушение изоляции.
  • Какой слой открывает транзакцию? Должен отвечать Application: сценарий — единица бизнес-операции. Транзакции в контроллере или в репозитории — признак перепутанных ролей.
  • Существует ли тест бизнес-правила, не поднимающий базу? Если нет — слой Domain уже зависит от инфраструктуры сильнее, чем кажется.

Варианты: 3-tier, 4-tier, N-tier

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

ТопологияРазвёртываниеГде живёт бизнес-логикаЭпоха и типичные системы
1-tierВсё на одной машинеВместе с даннымиМэйнфреймы; настольные приложения с локальной БД
2-tierКлиент + СУБДВ клиенте и хранимых процедурах1980–90-е: Delphi, PowerBuilder, Visual Basic
3-tierКлиент + сервер приложений + СУБДНа сервере приложений1990–2000-е: CORBA/DCOM/EJB, классические веб-приложения
4-tier+ Веб-сервер или интеграционное звеноРаспределена между звеньямиВеб-приложения 2000-х: браузер — веб — приложение — БД
N-tierCDN, балансировщик, шлюз, кэш, брокер, поиск…Распределена по специализированным звеньямОблачные системы 2010-х — наших дней

Каждый шаг этой лестницы — ответ на конкретную боль предыдущего шага: рост числа пользователей (нужен отдельный веб-звено), дорогая бизнес-логика на клиенте (трудно обновлять — нужен сервер приложений), нагрузка на БД (нужны кэш и реплики). Ни один шаг не отменяет внутренней слоистой организации кода — меняется только то, где слои физически исполняются.

3-tier: классическая трёхзвенная схема

Историческая и до сих пор самая распространённая форма: представление — бизнес-логика — данные. Родилась в клиент-серверную эпоху как выделение сервера приложений между толстым клиентом и СУБД: клиентская программа (или браузер с веб-сервером) отвечает за интерфейс, сервер приложений — за бизнес-логику, сервер БД — за хранение. Три звена масштабируются независимо: интерфейсных серверов может быть двадцать, серверов приложений — пять, база — одна реплицированная. Для корпоративных систем 1990–2000-х (и для огромного числа сегодняшних веб-приложений) 3-tier — достаточный и честный выбор: три роли, три причины изменения, три команды при необходимости.

В логическом чтении 3-tier чаще всего разворачивается в четыре кодовых слоя: «бизнес-слой» расщепляется на Application и Domain, поскольку сценарии и правила — разные виды логики. Физическая топология при этом может оставаться трёхзвенной: Application и Domain живут в одном процессе сервера приложений.

4-tier и спор о числе слоёв

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

  • Сервисный слой (Service Layer у Фаулера) над доменом — явная граница сценариев для внешних доставок; фактически это признание Application отдельным слоем.
  • Интеграционный слой — обёртки над внешними API и сообщениями, отделённые от доступа к собственной БД.
  • Инфраструктурный слой — «мешок» всего технологического: ORM, брокеры, файловые хранилища, часы, конфигурация.

В веб-развёртывании четырёхзвенность возникла сама собой: браузер — веб-сервер (статика, терминация TLS) — сервер приложений (бизнес-логика) — СУБД. Дальше число звеньев растёт с нагрузкой: кэш, очередь, полнотекстовый поиск, объектное хранилище. Внутренняя логическая стопка при этом может не меняться вовсе: новое звено — это чаще новая точка развёртывания существующего слоя.

N-tier как физическая топология

В облачную эпоху N-tier описывает уже не приложение, а ландшафт: CDN и балансировщики перед веб-звеном, API-шлюзы, кэши и брокеры между звеньями, выделенные БД и объектные хранилища позади. Каждое звено масштабируется, обновляется и наблюдается отдельно. Плата — сетевые издержки: каждый переход между звеньями — это сериализация, задержка и новая точка отказа, поэтому наивное «разнесём всё по звеньям» активно наказывается. Показателен исторический урок J2EE ранних версий: вынесение каждого entity-bean в сетевой вызов делало простейшие операции на порядки медленнее in-process-варианта, и спецификацию пришлось переписывать под локальные вызовы. Разумная N-tier-топология следует нагрузке, а не эстетике диаграммы; предельный случай децентрализации — микросервисы, где звенья превращаются в независимо поставляемые сервисы со своими данными.

Тонкий и толстый слой домена

Отдельный спектр внутри слоистой семьи — насыщенность слоя Domain. Фаулер в PoEAA выделил три способа организовать доменную логику, и выбор между ними — одно из главных решений слоистого проекта:

Transaction ScriptTable ModuleDomain Model
СтильПроцедуры: одна процедура — один сценарийОдин объект на таблицу, логика при нейСеть объектов с поведением
Слой DomainТонкий: часто вообще отсутствуетСредний: логика группируется по таблицамТолстый: сущности с инвариантами
Когда уместенПростые правила, CRUD, короткие сценарииТабличные данные, инструментальные системы (отчёты, АРМ)Сложные, изменчивые правила, долгоживущие системы
ЦенаДублирование логики между сценариямиПривязка к схеме БДПорог входа, маппинг, дисциплина

Тонкий домен — это признание, что бизнес-правил в системе немного: Application-сервисы содержат сценарии целиком, а «домен» вырождается в структуры данных. Для форм над базой и типовых CRUD-приложений это честно и дёшево. Толстый домен переносит правила в объекты: Order.canBeCancelled(), Tariff.calculate() — сущности сами знают свои инварианты, сценарии лишь оркестрируют их. Чем сложнее и изменчивее правила, тем дальше по спектру стоит двигаться; крайняя правая позиция спектра — тактические паттерны DDD (агрегаты, объекты-значения, доменные события), подробно разобранные в статье о DDD.

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

Спектр допускает и асимметрию внутри одной системы — этот путь открыло разделение команд и запросов (CQRS). На стороне запросов уместен тонкий домен: чтению нечего проверять и нечего инкапсулировать, поэтому обработчик запроса может спускаться от Presentation к Data Access почти напрямую, к оптимизированному представлению данных. На стороне команд домен обязан быть толстым: именно здесь инварианты и переходы состояний. Такое «лоскутное» распределение насыщенности по операциям — легальная и распространённая практика, снимающая главный упрёк к стилю («почему чтение должно проходить четыре слоя?»).

Плюсы и минусы

Плюсы

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

Разделение ответственности. Стиль напрямую реализует разделение ответственности Дейкстры: каждая роль — отдельный слой, у каждого слоя — одна причина изменения. Смена UI-фреймворка не затрагивает домен, смена СУБД — не затрагивает сценарии. Это свойство не гарантировано автоматически (см. минусы), но слоистая структура делает его достижимым и дешёвым для поддержки.

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

Несколько доставок на одну логику. Изоляция Presentation от нижних слоёв позволяет обслуживать веб-интерфейс, публичный API, консольные утилиты и обработчики очередей одним и тем же Application/Domain-кодом. Для каждой доставки пишется только тонкая обёртка — старейшая выгода слоистого разбиения, не потерявшая актуальности.

Соответствие фреймворкам. Mainstream-фреймворки устроены слоисто и поддерживают стиль «из коробки»: Spring (контроллеры — сервисы — репозитории), ASP.NET, Django (views — сервисы — models/ORM), Rails. Скаффолдинг, DI-контейнеры, транзакционные менеджеры и учебные материалы — всё предполагает слоистую форму, поэтому стартовая стоимость стиля близка к нулю.

Организация команд по слоям. Роли слоёв естественно ложатся на специализацию команды: фронтенд-разработчики — на Presentation, бэкенд — на Application/Domain, специалист по данным — на Data Access. Для многих организаций это работающая структура владения кодом (хотя следует помнить о законе Конвея: разбиение по техническим слоям размазывает бизнес-фичи по всем командам — об этом в минусах).

Инкрементальная эволюция. Стопка хорошо реагирует на рост сложности: интеграционный слой, кэширующий репозиторий, отдельная сборка домена, CQRS-развилка чтения и записи — всё это локальные операции над одним слоем, не требующие перестройки системы. Слоистая архитектура редко бывает конечной станцией, но почти всегда — удобной промежуточной: из неё можно шагнуть и в модульный монолит (проведя вертикальные границы), и в Clean Architecture (инвертировав зависимости), и в микросервисы (разрезав звенья).

Минусы

«Протекание» домена в инфраструктуру. Главный хронический дефект практики. Правило «вниз» означает, что домен зависит от доступа к данным — и в реальных кодовых базах бизнес-правила постепенно расползаются туда, где их удобнее писать: в SQL-запросы и хранимые процедуры, в ORM-хуки, в контроллеры. Система формально сохраняет слои, но бизнес-логика перестаёт быть локализованной: чтобы понять правило расчёта скидки, нужно читать четыре файла в трёх слоях. Смена СУБД или ORM при таком протекании превращается в проект по переписыванию всей системы.

Неблагоприятное направление зависимостей. Фундаментальный, а не бытовой минус: в классической стопке самый ценный код (домен) зависит от самого изменчивого (инфраструктуры). Именно этот дефект исправили наследники стиля — Hexagonal, Onion, Clean Architecture — инвертировав зависимость домена от доступа к данным. Механика переворота — Dependency Inversion из SOLID — разобрана в следующем разделе.

Сквозные проблемы. Логирование, авторизация, кэширование, метрики, транзакционность нужны всем слоям сразу, но места для них в горизонтальной стопке нет: каждый слой решает их заново или дублирует. Показательна история самой Java: сквозная логика в EJB была такой болью, что породила целое направление AOP (аспектно-ориентированное программирование) с перехватчиками и «переплетением» кода. Полностью проблема не решена до сих пор: сквозные механизмы остаются внешними по отношению к слоистой модели.

Каскад изменений и трансляция данных. Одно бизнес-изменение («добавить поле промокода к заказу») проходит через всю стопку: колонка в БД, маппинг ORM, доменная структура, DTO сценария, view model, интерфейс. Каждый переход между слоями — это преобразование формата данных, и на зрелых слоистых системах таких преобразований накапливается больше, чем самой логики. Маппинг — осознанная цена изоляции, но в классической стопке она часто платится без получения выгоды: пять форматов одного поля при полном отсутствии заменяемости слоёв.

Тестируемость без инфраструктуры затруднена. Если домен зависит от ORM, а сервис — от репозиториев с конкретными реализациями, юнит-тест бизнес-правил требует либо поднятой базы, либо трудоёмкого mock-хозяйства. Типичное следствие — «пирамида тестирования» выворачивается: интеграционных тестов с базой больше, чем быстрых юнит-тестов правил, и регрессионная сетка становится медленной и хрупкой. Радикальное лечение — инверсия зависимостей — это уже выход за пределы классического стиля.

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

Горизонтальное разбиение против вертикальных фич. Слои режут систему по техническим ролям, а изменения приходят по бизнес-фичам: «корзина», «каталог», «биллинг» размазаны по всем слоям тонкими ломтиками. Одна фича = правки во всех слоях + координация всех профильных команд. Именно это несоответствие стало мотивом вертикальных подходов — модульного монолита и микросервисов, где границы проводятся по бизнес-возможностям; подробнее — в статье о монолитной архитектуре.

Типичные антипаттерны

Anemic Domain Model (анемичная доменная модель). Слой Domain формально существует, но состоит из структур с геттерами и сеттерами; вся логика живёт в сервисах Application. Мартин Фаулер описал это в 2003 году как антипаттерн: «объекты-записи плюс процедуры-скрипты» — процедурное программирование, замаскированное под ООП и потерявшее его главную выгоду — инкапсуляцию данных с поведением. Инварианты (например, «заказ нельзя оплатить дважды») никем не удерживаются: любой код может привести «сущность» в любое состояние через сеттер. Анемия — самый массовый исход слоистых проектов: стиль сам по себе её не требует, но поощряемое им разделение «данные отдельно, сценарии отдельно» регулярно в неё скатывается.

Протекание домена (domain leakage). Бизнес-правила написаны на языке чужого слоя: расчёты в SQL-запросах и хранимых процедурах, валидация в контроллерах, форматирование в шаблонах. Отдельный случай — ORM-атрибуты на доменных сущностях: домен превращается в схему таблицы с методами. Диагностика простая: если для ответа на вопрос «где живёт правило X» нужно назвать больше одного слоя, домен протёк.

Обход слоёв (layer bypass). Presentation вызывает Data Access напрямую, «потому что для простого списка незачем дёргать сервис». Единичные исключения зреют в систему: изоляция слоёв — свойство, которое не выдерживает частичных нарушений. После первого обхода следующие аргументы «а чем мой случай хуже» неотразимы, и через год слой Application превращается в декорацию для половины операций.

God-service. Один сервис Application на всю предметную область (UserService, OrderService на тысячи строк), собирающий все сценарии. Слои выдержаны, но внутри слоя исчезла вся структура: сервис — это процедурный модуль с внутренней связностью на уровне «общая база данных». Лечение — дробление сервисов по намерениям пользователей (placeOrder, cancelOrder), как в интеракторах Clean Architecture.

База как API. Звенья и приложения интегрируются через общие таблицы, минуя слои доступа к данным друг друга: «прочитаю твою таблицу — быстрее, чем через твой сервис». Такая экономия связывает участников схемой БД наглухо: любое изменение таблицы чужим кодом невидимо, и система превращается в распределённый монолит, соединяющий сложность распределённой системы с негибкостью монолита, — подробнее в статье о монолитной архитектуре.

Слои-папки. Структура директорий создана, но зависимости никто не контролирует: домен импортирует ORM, контроллер — репозиторий, тест — внутренности сервиса. Это косметическая слоистость: диаграмма красивая, правило зависимостей отсутствует. Без автоматических проверок (ArchUnit, NetArchTest, dependency-cruiser в CI) любая слоистая структура дрейфует сюда за считанные месяцы.

«Лазанья». Избыточное число слоёв для простой системы: отдельные DTO на каждый слой, мапперы мапперов, «сервис-прослойка», пересылающая вызов репозиторию без единой строки логики. Симптом: изменение одного поля требует правок в шести файлах. Правило то же, что и в Clean Architecture: число слоёв должно соответствовать числу реальных причин изменения, а не амбициям диаграммы.

Слоистая архитектура vs Clean / Hexagonal

Слоистая архитектура и её знаменитые наследники — Hexagonal (Коубёрн, 2005), Onion (Палермо, 2008), Clean Architecture (Мартин, 2012/2017) — описывают почти одинаковую картину: система делится на области по ответственности, между областями проводятся границы. Различие одно, но фундаментальное: направление зависимости между доменом и инфраструктурой.

КритерийСлоистая (классическая)Clean / Hexagonal / Onion
Правило зависимостейСтрого сверху внизСтрого внутрь, к домену
Зависимость домена от инфраструктурыДопускается: Domain зависит от Data AccessЗапрещена: инфраструктура зависит от домена
Где живёт интерфейс доступа к даннымВ нижнем слое, вместе с реализациейВ домене; реализация подключается снаружи
Механизм границыПакеты, конвенции, внешний контрольDependency Inversion: интерфейс внутреннего слоя + внешняя реализация
Юнит-тест доменаЧасто требует базы или мок-инфраструктурыЗаглушки интерфейсов, база не нужна
Смена ORM/СУБДПроект: затрагивает домен и сервисыЛокальна: меняется внешний адаптер
Стоимость входаМинимальна, поддерживается фреймворкамиИнтерфейсы, маппинг, DI, дисциплина
Естественная нишаCRUD, формы над данными, простые правилаСложная, изменчивая доменная логика, долгоживущие системы

Суть поворота: наследники не отменили слои — они поменяли одну стрелку. В классической стопке домен объявляет о своих потребностях нижнему слою тем, что импортирует его типы; в чистой — домен объявляет собственный интерфейс (OrderRepository), а инфраструктура его реализует и, следовательно, зависит от домена. Вызов в рантайме по-прежнему идёт «вниз» (сервис вызывает сохранение в базу), но зависимость в коде развёрнута вверх ногами — это и есть Dependency Inversion Principle, применённый на архитектурном масштабе. Механика, круги Clean Architecture и история подхода подробно разобраны в статье Clean Architecture.

Что выбрать

Практический вывод из сравнения — не соперничество подходов, а показания к применению.

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

  • доменная логика проста и стабильна: CRUD, справочники, формы над базой, административные панели;
  • горизонт жизни системы ограничен, а команда небольшая — инвестиции в границы не успеют окупиться;
  • фреймворк уже навязывает слоистую форму (Spring, ASP.NET, Django, Rails), и проще следовать ей, чем бороться;
  • система живёт внутри одного продукта с одной-двумя доставками.

Переход к Clean Architecture, Hexagonal или Onion (либо точечная инверсия зависимостей внутри слоистой стопки) оправдан, когда:

  • бизнес-правила сложны, многочисленны и часто меняются — протекание домена в SQL и ORM становится главным источником стоимости изменений;
  • у одной логики несколько доставок: веб, публичный API, консоль, очередь — изоляция Domain/Application от Presentation перестаёт быть теоретической;
  • ошибки бизнес-правил дороги (финансы, медицина, договоры) и нужна плотная сетка быстрых юнит-тестов именно на правила;
  • горизонт жизни измеряется годами и смена ORM/СУБД/UI-фреймворка — не гипотеза, а плановое событие.

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

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

Связанные статьи

  • Clean Architecture — прямой потомок слоистого стиля: те же зоны ответственности, но правило зависимостей перевёрнуто внутрь; сравнение подходов и история поворота.
  • DDD — наполнение толстого слоя Domain: тактические паттерны (агрегаты, объекты-значения, репозитории) как способ борьбы с анемичной моделью.
  • Монолитная архитектура — слоистая структура как типичный внутренний строй монолита; модульный монолит как вертикальная альтернатива горизонтальным слоям.
  • Микросервисы — предельная форма N-tier-топологии: звенья развёртывания превращаются в независимо поставляемые сервисы со своими данными.
  • SOLID — принципы уровня классов, масштабирующиеся до слоёв; Dependency Inversion как механизм перехода от «вниз» к «внутрь».
  • High-Level Design — уровень проектирования, на котором принимается решение о числе слоёв и звеньев развёртывания.
  • Паттерны проектирования — строительные блоки внутри слоёв: сервис, репозиторий, DTO, стратегия и другие решения повторяющихся задач.
  • Введение в архитектуру — общие понятия архитектуры ПО и место слоистого стиля среди архитектурных подходов.