Слоистая архитектура приложений: как обеспечить поддерживаемость доменного слоя

Введение

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

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

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

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

7bd5d1082229abc635f61a044e90cf08.png

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

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

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

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

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

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

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

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

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

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

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

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

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

fd789b908dd72507089c52ed33a5f498.png

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

bf7ff68667d7b8afb182e117cd4ff2db.png

Заключение

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

© Habrahabr.ru