Введение
Слоистая архитектура – это популярный подход к разработке программного обеспечения, призванный облегчить цикл создания, тестирования, поддержки и обновления приложений. Но его неумелое использование может привести к результатам прямо противоположным: нечитаемому коду, неконтролируемым зависимостям и невозможности тестирования. В этой статье я расскажу, как избежать типичных проблем при использовании слоистой архитектуры, чтобы исключить крах проекта и создать эффективное приложение.
Что такое слоистая архитектура?
Слоистая архитектура предлагает деление программного обеспечения на отдельные уровни (слои), каждый из которых выполняет строго определенный набор функций. Главная цель такого подхода - обеспечить независимость и модульность компонентов, а также четкую организацию кода для оптимизации разработки, масштабирования и поддержки приложений.
Посмотрим на типичное деление функциональностей приложения на слои, которое можно часто увидеть на практике:
Presentation layer (Презентационный слой)
Слой, который взаимодействует непосредственно с пользователем, называется презентационным. Он отвечает за представление информации и адаптируется под различные платформы, например, веб и мобильные приложения, без изменения бизнес-логики. На этом слое, среди прочего, располагаются пользовательский интерфейс (UI) и контроллеры. Последние отвечают за обработку входящих запросов от пользователей или других систем, управление потоком данных и координацию действий между пользовательским интерфейсом и бизнес-логикой приложения.
Business layer (Бизнес-слой)
Бизнес-слой - это связующее звено между презентационным слоем и слоем доступа к данным. Среди его задач - обработка бизнес-правил, запросов и операций, связанных с данными, полученными от пользователя. На этом уровне данные подготавливаются к сохранению или дальнейшей обработке.
В доменно-ориентированном проектировании (Domain-Driven Design, DDD) ключевую роль в реализации бизнес-логики играют доменные сервисы. Они инкапсулируют бизнес-операции, которые требует совместной работы нескольких различных доменных объектов, - сущностей и агрегатов, - чтобы оптимизировать их работу. К таким операциям относятся расчеты, которые опираются на данные из нескольких источников, а также бизнес-правила, требующие валидации или согласования между объектами.
Например, операция "Перевод денег с одного счета на другой" может включать проверку условий перевода, выполнение самого перевода и регистрацию транзакции в журнале. Эти действия требуют взаимодействия между различными агрегатами, - счета, журнал транзакций, - поэтому логично вынести их в доменный сервис.
Persistence/Integration layer (Слой персистентности/интеграции)
Задача этого слоя - взаимодействие с внешними сервисами, системами и базами данных. Он абстрагирует работу с данными, позволяя бизнес-слою не зависеть от конкретных механизмов хранения или внешних сервисов. Здесь находятся клиенты для других бэкендов и баз данных, которые выполняют различные функции. Рассмотрим некоторые из них:
Абстракция доступа к данным: Клиенты баз данных предоставляют унифицированный интерфейс для работы с различными системами управления базами данных (СУБД), скрывая специфику SQL-запросов или API конкретных баз данных. На практике это упрощает переход на новую СУБД и изменение схемы данных без значительного вмешательства в бизнес-логику приложения.
Интеграция с внешними сервисами: Клиенты для внешних API предоставляют абстрактный слой для взаимодействия с внешними системами через протоколы типа REST, SOAP, gRPC, упрощая интеграцию с другими сервисами и платформами. Таким образом, приложение получает возможность использовать функционал внешних сервисов, например, платежных систем, сервисов геолокации или социальных сетей.
Управление ресурсами: Клиенты так же обеспечивают эффективное использование сетевых соединений, пулов соединений к базам данных и обработку исключений при взаимодействии с внешними системами. Это включает в себя механизмы кеширования, повторных попыток подключения и логирования, что повышает надежность и производительность приложения.
Проблемы слоистой архитектуры
Среди ключевых проблем типичного использования слоистой архитектуры - взаимозависимость модулей в бизнес-слое. Часто каждый доменный сервис может свободно зависеть от любого другого доменного сервиса, вызывать его методы и использовать его модели без каких-либо ограничений. В таких случаях часто отсутствует какая либо иерархия или структура связей между доменными сервисами.
В данной реализации архитектура управления бизнес-логикой напоминает модель акторов, где каждый доменный сервис можно представить как одного из акторов. Модель акторов в явном виде резко набрала популярность несколько лет назад, в scala сообществе благодаря библиотеке akka. Однако она так же быстро растеряла свою популярность, потому что ее использование вызывало ряд специфических проблем. Рассмотрим ее в деталях, чтобы понять, как выстроить оптимальное решение.
В этой модели каждый "актор" представляет собой сущность, способную выполнять вычисления, хранить состояние и обмениваться сообщениями с другими акторами асинхронно. Такая модель удобна для разработки распределенных систем с высокой степенью изоляции и надежности. При этом, плохая структура взаимодействия как между акторами так и между доменными сервисами может значительно усложнить архитектуру, что может приводить к ряду проблем при разработке и поддержании приложений. Среди них следующие:
Явные и неявные циклические зависимости
Возможность каждого модуля взаимодействовать с любым другим модулем без четкой структуры и иерархии может привести к сложной и запутанной сети связей. В частности, возможно появление циклических зависимостей, которые могут привести к бесконечной рекурсии и взаимной блокировке, что делает отладку и изменение поведения доменных сервисов более сложными задачами.
Отсутствие общего состояния
В такой архитектуре каждый доменный сервис моделирует лишь часть процесса и хранит только часть его состояния. Обычно отсутствует единая точка, которая бы хранила состояние всего процесса. В результате для отладки состояния процесса разработчику необходимо либо держать всю систему в уме, либо прибегать к использованию дополнительных инструментов для восстановления состояния всей системы на конкретный момент времени в прошлом.
Сложность рефакторинга
Архитектура системы, построенной на модели акторов, характеризуется высокой связностью и сложностью взаимодействий между компонентами. Это делает любые изменения проблематичными, так как до конца не ясно, как изменения в одном модуле повлияют на другие и систему в целом.
Сложность юнит-тестирования
Для качественного юнит-тестирования необходимо иметь хорошо выделенные "юниты". Однако в архитектуре с распределенным состоянием и распределенной ответственностью размываются границы ответственности между "юнитами". Это снижает качество юнит-тестирования и приводит к необходимости тестирования большого количества инвариантов между модулями. В таких случаях часто требуется больше усилий для интеграционного тестирования, которое, как известно, более затратно, чем юнит-тестирование.
Решение проблем: Разделение бизнес-слоя на 2 уровня
Решением перечисленных проблем может быть разделение бизнес-слоя приложения на два уровня. Рассмотрим эти уровни:
Уровень бизнес-процессов
Этот уровень отвечает за реализацию конкретных бизнес-процессов, которые есть в системе. Каждый модуль на этом уровне специализируется на определённом бизнес-процессе, например, "получение профиля пользователя". Это включает в себя все шаги, необходимые для выполнения процесса, от начала до конца.
Модули функционируют независимо друг от друга, избегая прямых вызовов или зависимостей между различными процессами, что обеспечивает чёткое разделение ответственности и упрощает управление изменениями. При этом для поддержания изоляции допускается частичное дублирование логики между похожими процессами.
Уровень бизнес-домена
Обеспечением работы бизнес-процессов занимаются универсальные компоненты и сервисы на уровне бизнес-домена. К ним относятся общие сущности, сервисы, утилиты, например, компоненты для работы с пользовательскими данными, финансами, процессами аутентификации и авторизации. Перемещение общих сервисов и компонентов на уровень домена позволяет использовать код повторно, а также реализовывать общие функции по всему приложению согласованно. При этом модули на доменном уровне остаются изолированными, несмотря на то, что могут использовать данные и модели друг друга.
Двухуровневая структура, разделяющая общие доменные сервисы и уникальные бизнес-процессы, облегчает масштабирование приложения, улучшая при этом управляемость кодом и снижая сложность системы.
Свой аналог уровня бизнес-процессов есть и у Microsoft. Корпорация рекомендует создавать Application layer на презентационном уровне, где также находятся описание API и модель представления. Но, по моему опыту, такое решение часто путает разработчиков и приводит к смешению view-моделей и предметных моделей бизнес-процессов.
Пример разделения
Используя этот подход, давайте переработаем слоистую архитектуру приложения, которую я описал выше.
Presentation Layer (Контроллеры): Остается без изменений, поскольку его основная роль — взаимодействие с пользователем — не меняется.
-
Business Layer: Разделен на два уровня:
Уровень Бизнес Процессов: Каждый модуль здесь реализует определенный бизнес-процесс, например, модуль "Управление профилем пользователя", который включает в себя функции для получения, обновления и удаления профиля пользователя.
Уровень Бизнес Домена: Содержит модули, предоставляющие общие функции и классы, такие как "Пользователь", "Аутентификация" и "Авторизация", которые используются различными модулями на уровне процессов.
Persistence/Integration Layer: По-прежнему отвечает за взаимодействие с базами данных и интеграцию с внешними сервисами, используя интерфейсы, определенные на уровне домена. Это обеспечивает разделение ответственности и упрощает тестирование и поддержку.
Заключение
В этом обзоре я рассмотрел ключевые проблемы слоистой архитектуры и предложил действенное решение для их устранение. Оно заключается в разделении бизнес-уровня на уровень домена и уровень процессов, что позволяет повысить гибкость и масштабируемость системы. Этот подход может обеспечить легкость поддержки и развития приложений, а также помочь создавать более стабильные и адаптивные к изменениям системы.
Batalmv
Да ничего оно не дает, кроме сосредоточения "бардака" в одном слое и такой-же кучи зависимостей
Тот же Микрософт дает куда более интересную концепцию: https://learn.microsoft.com/en-us/dotnet/architecture/modern-web-apps-azure/common-web-application-architectures#clean-architecture
Не свою понятно, но с указанием, как разложить по ASP.NET Core и всякие там "заморочки" на уровне IDE
А эта концепция бизнес слоя давно кочует по планете. К примеру, системы класса BPM, ESB также реализуют эту концепцию, что работает на сложном legacy-environment большой конторы, где куда не вступи - все в овно мамонта вступишь :)
pahomovda Автор
Соглашусь, что это не серебряная пуля, и без поддержки качества кода никакая методика работать не будет. Однако, наивный подход "все взаимодействуют со всеми" просто не дает возможности для организации хорошей иерархии взаимодействий, что ведет к помойке сразу со старта разработки, как за качеством не следи.
Методики Microsoft хороши, но они слабо распространены за пределами стека microsoft, и описанную проблему, например, они рекомендуют решать на presentational уровне, что завязывает описание процесса на вью модель и требует впоследствии рефакторинга и разделения "настоящей" вью модели и модели процесса, что собственно и описывается в статье.