Слоистая архитектура
Слоистая архитектура (Layered Architecture) делит приложение на горизонтальные слои, у каждого из которых одна техническая роль: показать данные пользователю, применить бизнес-правила, сохранить и прочитать данные. Слой обращается только к слою под ним, а нижние слои ничего не знают о верхних. Поэтому любой слой можно понять, протестировать и заменить, не разбираясь в остальных.
Проблема
В приложении без структуры обработчик кнопки сам проверяет ввод, сам собирает SQL-запрос, сам решает, можно ли регистрировать пользователя, и сам рисует ответ. Бизнес-правило вроде «email должен быть уникальным» оказывается размазано по экранам, и форма регистрации, API и импорт из файла проверяют его каждый по-своему. Смена базы данных или фреймворка интерфейса превращается в переписывание всего кода, потому что у каждой строки есть зависимость от всего сразу.
Решение
Код раскладывается по слоям сверху вниз. Слой представления принимает ввод и показывает результат, но правил не знает. Бизнес-слой (сервисы) содержит правила предметной области и решает, что делать. Слой хранения (репозитории) переводит вопросы бизнес-слоя в запросы к базе и возвращает готовые объекты. База данных хранит сами данные. Каждый слой вызывает только соседа снизу, а результат возвращается обратно тем же путём, как значение вызова - нижний слой никогда сам не обращается к верхнему.
Как это работает
Идея слоёв старше программной архитектуры как дисциплины. В 1968 году Эдсгер Дейкстра описал операционную систему THE (Technische Hogeschool Eindhoven) как иерархию из шести уровней, от распределения процессора и памяти внизу до программ пользователя и оператора наверху. Каждый уровень пользовался только услугами уровней ниже, и благодаря этому систему можно было проверять по частям, снизу вверх.
Самый известный пример слоёв - сетевые протоколы. Модель OSI (стандарт ISO 7498, 1984) насчитывает семь уровней, практический стек TCP/IP - четыре. Каждый уровень даёт верхнему сервис через чёткий интерфейс: HTTP не знает, идут ли пакеты по Ethernet или по Wi-Fi, и поэтому физическую сеть можно менять, не трогая браузер.
Как архитектурный паттерн Layers описан в книге «Pattern-Oriented Software Architecture» (Бушман и соавторы, 1996). Там же различаются строгий вариант, где слой обращается только к соседу снизу, и ослабленный (Relaxed Layered System), где слой может обратиться к любому нижележащему. Ослабленный вариант гибче и может работать быстрее, но слабее изолирует слои: изменение внизу может задеть сразу несколько верхних.
В корпоративных приложениях число слоёв разное, а правило одно. Мартин Фаулер в «Patterns of Enterprise Application Architecture» (2002) выделяет три основных слоя: представление, предметная область и источник данных. Эрик Эванс в «Domain-Driven Design» (2003) - четыре: интерфейс пользователя, прикладной слой, предметная область и инфраструктура. Марк Ричардс в «Software Architecture Patterns» (O'Reilly, 2015) использует вариант со схемы: представление, бизнес-логика, хранение и база данных.
Ричардс называет слой закрытым, если запрос обязан через него пройти, и открытым, если его можно обойти. Обычно все слои закрыты: представление не может обратиться к репозиторию напрямую, даже когда сервис ничего не добавляет. Так получается изоляция слоёв - изменение в слое хранения не доходит до представления. Открытым иногда делают вспомогательный слой общих сервисов, к которому бизнес-слой обращается по необходимости.
У закрытых слоёв есть цена - антипаттерн «архитектурная воронка» (architecture sinkhole): запрос проходит все слои, и ни один из них ничего с ним не делает, кроме передачи дальше. Ричардс предлагает правило 80/20: если так проходят около 20% запросов, это нормально, а если около 80%, слоистая архитектура, скорее всего, не подходит задаче.
Слои не надо путать с уровнями (tiers). Слой - это логическое разделение кода, уровень - физическое: отдельная машина или процесс, как в трёхуровневой архитектуре «браузер - сервер приложений - сервер базы данных». В примере на вкладке «Реализация» все четыре слоя работают в одной вкладке браузера, и это всё ещё слоистая архитектура.
Главная критика классической схемы - направление зависимости: бизнес-слой зависит от слоя хранения, поэтому правила предметной области привязаны к хранилищу. Гексагональная архитектура (Алистер Кокберн, 2005), луковая (Джеффри Палермо, 2008) и чистая (Роберт Мартин, 2012) разворачивают эту зависимость: в центре предметная область, а база данных и интерфейс становятся внешними деталями. Вторая претензия - разделение по технической роли: Ричардс и Нил Форд в «Fundamentals of Software Architecture» (2020) противопоставляют ему разделение по предметным областям, при котором одна функция продукта живёт в одном модуле, а не размазана по всем слоям.
Нюансы выбора
Отправная точка - если неясно, какой стиль выбрать, слоистая архитектура почти всегда приемлема для начала: она проста, всем знакома и не мешает позже вынести предметную область в центр, как в гексагональной архитектуре.
Строгие или ослабленные слои - строгий вариант надёжнее изолирует изменения, ослабленный экономит вызовы. Если разрешаете обход, делайте его явным исключением для конкретного слоя (открытый слой), а не общим правилом «кто как хочет».
Против слоёв - если приложение почти целиком состоит из «прочитать запись и показать её», большинство запросов пройдёт через сервисы насквозь. Здесь слои дают церемонии без пользы, и проще обойтись двумя слоями или готовым CRUD-фреймворком.
Против слоёв, когда правила сложные - если предметная область богатая и живёт долго, а хранилищ и интерфейсов несколько, зависимость бизнес-слоя от хранения начинает мешать. Тогда лучше подходят гексагональная или чистая архитектура, где правила не зависят ни от базы, ни от экрана.
Большая система и много команд - когда над продуктом работают десятки человек и каждая задача задевает все слои, команды начинают мешать друг другу. Это сигнал перейти к разделению по предметным областям (модульный монолит, затем, возможно, микросервисы).
Примеры и история
Edsger W. Dijkstra, «The Structure of the "THE"-Multiprogramming System» (Communications of the ACM, 1968) - статья, где операционная система впервые описана как иерархия уровней, каждый из которых пользуется только нижними.
Frank Buschmann и соавторы, «Pattern-Oriented Software Architecture, Volume 1» (1996) - каноническое описание паттерна Layers со строгим и ослабленным вариантами.
Martin Fowler, «Patterns of Enterprise Application Architecture» (2002) - глава о разделении на слои представления, предметной области и источника данных, а также паттерн Service Layer.
Mark Richards, «Software Architecture Patterns» (O'Reilly, 2015) - короткая книга, откуда взяты закрытые и открытые слои, изоляция слоёв и антипаттерн «архитектурная воронка».
Руководство по архитектуре Android-приложений (Google) - рекомендует слой интерфейса, необязательный слой предметной области и слой данных; зависимости направлены от интерфейса к данным, а не наоборот.
Когда применять
- Когда начинается обычное бизнес-приложение - учёт, личный кабинет, внутренняя система - и требования ещё не устоялись. Слоистая архитектура даёт понятный порядок без лишних абстракций, и от неё легко перейти к более строгим стилям.
- Когда команда разделена по специализациям (интерфейс, бэкенд, база данных) и каждой группе удобно отвечать за свой слой.
Примеры из практики
- Spring - типичное Spring-приложение раскладывает классы по аннотациям
@Controller,@Serviceи@Repository, которые прямо соответствуют слоям представления, бизнес-логики и хранения. - Сетевой стек - модель OSI и стек TCP/IP устроены слоями: HTTP работает поверх TCP, TCP поверх IP, IP поверх Ethernet или Wi-Fi, и замена нижнего слоя не требует менять верхние.
Похожие паттерны
Гексагональная архитектура
Гексагональная архитектура (Hexagonal Architecture), её второе название - Порты и адаптеры (Ports and Adapters), ставит бизнес-правила в центр приложения и отделяет их от всего внешнего: интерфейса, базы данных, тестов. Ядро объявляет порты - интерфейсы, через которые с ним можно говорить и через которые оно само просит данные, - а снаружи к портам подключаются адаптеры. Все зависимости направлены внутрь, к ядру, поэтому интерфейс или хранилище можно заменить другим адаптером того же порта, не меняя ни строчки в правилах.
Архитектура микроядра
Архитектура микроядра (Microkernel Architecture), её также называют архитектурой плагинов (plug-in architecture), делит приложение на маленькое ядро с минимумом функций и подключаемые к нему плагины. Ядро знает, какие плагины установлены, и вызывает их через общий контракт, а каждая новая функция или правило продукта приходит отдельным плагином. Поэтому возможности продукта растут и настраиваются под конкретного клиента подключением и отключением плагинов, а не правкой ядра - пока новый плагин укладывается в контракт ядра.
Клиент-серверная архитектура
Клиент-серверная архитектура (Client-Server Architecture) делит систему на две роли, которые работают в разных процессах и обычно на разных машинах: клиенты отправляют запросы, а сервер их обрабатывает и отвечает. Данные и правила, которым можно доверять, живут на сервере, и он - единственный источник истины сразу для всех клиентов. Клиент бывает тонким или толстым, но последнее слово в том, чему можно доверять, всегда за сервером. Между ними лежит сеть, поэтому каждое сообщение сериализуется, идёт с задержкой и может потеряться.