Введение

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

Что такое слоистая архитектура?

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

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

Presentation layer (Презентационный слой)

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

Business layer (Бизнес-слой)

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

В доменно-ориентированном проектировании (Domain-Driven Design, DDD) ключевую роль в реализации бизнес-логики играют доменные сервисы. Они инкапсулируют бизнес-операции, которые требует совместной работы нескольких различных доменных объектов, - сущностей и агрегатов, - чтобы оптимизировать их работу. К таким операциям относятся расчеты, которые опираются на данные из нескольких источников, а также бизнес-правила, требующие валидации или согласования между объектами.

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

Persistence/Integration layer (Слой персистентности/интеграции)

Задача этого слоя - взаимодействие с внешними сервисами, системами и базами данных. Он абстрагирует работу с данными, позволяя бизнес-слою не зависеть от конкретных механизмов хранения или внешних сервисов. Здесь находятся клиенты для других бэкендов и баз данных, которые выполняют различные функции. Рассмотрим некоторые из них:

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

  2. Интеграция с внешними сервисами: Клиенты для внешних API предоставляют абстрактный слой для взаимодействия с внешними системами через протоколы типа REST, SOAP, gRPC, упрощая интеграцию с другими сервисами и платформами. Таким образом, приложение получает возможность использовать функционал внешних сервисов, например, платежных систем, сервисов геолокации или социальных сетей.

  3. Управление ресурсами: Клиенты так же обеспечивают эффективное использование сетевых соединений, пулов соединений к базам данных и обработку исключений при взаимодействии с внешними системами. Это включает в себя механизмы кеширования, повторных попыток подключения и логирования, что повышает надежность и производительность приложения.

Проблемы слоистой архитектуры

Среди ключевых проблем типичного использования слоистой архитектуры - взаимозависимость модулей в бизнес-слое. Часто каждый доменный сервис может свободно зависеть от любого другого доменного сервиса, вызывать его методы и использовать его модели без каких-либо ограничений. В таких случаях часто отсутствует какая либо иерархия или структура связей между доменными сервисами.

В данной реализации архитектура управления бизнес-логикой напоминает модель акторов, где каждый доменный сервис можно представить как одного из акторов. Модель акторов в явном виде резко набрала популярность несколько лет назад, в scala сообществе благодаря библиотеке akka. Однако она так же быстро растеряла свою популярность, потому что ее использование вызывало ряд специфических проблем. Рассмотрим ее в деталях, чтобы понять, как выстроить оптимальное решение.

В этой модели каждый "актор" представляет собой сущность, способную выполнять вычисления, хранить состояние и обмениваться сообщениями с другими акторами асинхронно. Такая модель удобна для разработки распределенных систем с высокой степенью изоляции и надежности. При этом, плохая структура взаимодействия как между акторами так и между доменными сервисами может значительно усложнить архитектуру, что может приводить к ряду проблем при разработке и поддержании приложений. Среди них следующие:

Явные и неявные циклические зависимости

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

Отсутствие общего состояния

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

Сложность рефакторинга

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

Сложность юнит-тестирования

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

Решение проблем: Разделение бизнес-слоя на 2 уровня

Решением перечисленных проблем может быть разделение бизнес-слоя приложения на два уровня. Рассмотрим эти уровни:

Уровень бизнес-процессов

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

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

Уровень бизнес-домена

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

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

Свой аналог уровня бизнес-процессов есть и у Microsoft. Корпорация рекомендует создавать Application layer на презентационном уровне, где также находятся описание API и модель представления. Но, по моему опыту, такое решение часто путает разработчиков и приводит к смешению view-моделей и предметных моделей бизнес-процессов.

Пример разделения

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

  • Presentation Layer (Контроллеры): Остается без изменений, поскольку его основная роль — взаимодействие с пользователем — не меняется.

  • Business Layer: Разделен на два уровня:

    • Уровень Бизнес Процессов: Каждый модуль здесь реализует определенный бизнес-процесс, например, модуль "Управление профилем пользователя", который включает в себя функции для получения, обновления и удаления профиля пользователя.

    • Уровень Бизнес Домена: Содержит модули, предоставляющие общие функции и классы, такие как "Пользователь", "Аутентификация" и "Авторизация", которые используются различными модулями на уровне процессов.

  • Persistence/Integration Layer: По-прежнему отвечает за взаимодействие с базами данных и интеграцию с внешними сервисами, используя интерфейсы, определенные на уровне домена. Это обеспечивает разделение ответственности и упрощает тестирование и поддержку.

Заключение

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

Комментарии (2)


  1. Batalmv
    25.03.2024 18:02
    +1

    Да ничего оно не дает, кроме сосредоточения "бардака" в одном слое и такой-же кучи зависимостей

    Тот же Микрософт дает куда более интересную концепцию: 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 большой конторы, где куда не вступи - все в овно мамонта вступишь :)


    1. pahomovda Автор
      25.03.2024 18:02
      +3

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

      Методики Microsoft хороши, но они слабо распространены за пределами стека microsoft, и описанную проблему, например, они рекомендуют решать на presentational уровне, что завязывает описание процесса на вью модель и требует впоследствии рефакторинга и разделения "настоящей" вью модели и модели процесса, что собственно и описывается в статье.