Назад к списку

Подготовка backend-a. Чистая архитектура

Итак, начнём с заготовки блока BE Proxy, который впоследствии должен вытеснить серверную часть изначального приложения.

На первом этапе это будет всего несколько бизнес-сущностей, но нам необходимо заложить базовые архитектурные принципы, чтобы проект можно было легко масштабировать. За основу возьмём принципы чистой архитектуры (clean architecture). 

Что такое чистая архитектура и почему стоит следовать её принципам?

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

  • низкую связанность компонентов;
  • устойчивость к изменениям бизнес-логики;
  • разделение приложения на логические блоки;
  • удобство сопровождения и расширения (всё-таки это в некотором роде «стандарт», принципы которого понятны другим разработчикам);
  • независимость от фреймворка/UI/БД/внешнего сервиса (можно легко перейти с MS SQL Server на Postgres или с UI на ASP.NET на React и т.д.).

Принципы чистой архитектуры

Можно сказать, что принципы чистой архитектуры имеют «domain first» подход, т.е. предлагают строить приложение вокруг бизнес-логики.
Разделение на уровни:



Рис. № 1

  • уровень домена (domain) – содержит сущности, отвечающие за работу с данными;
  • уровень приложения (application) – выполняет бизнес-логику и координирует работу между уровнями представления и домена;
  • уровень инфраструктуры – связь с внешними системами (БД, API etc);
  • уровень представления – взаимодействие с пользователем.

Зависимости внутри уровней

Направление зависимостей соответствует направлению стрелок на рисунке № 1. Один из способов это сделать – инверсия зависимостей (Dependency Inversion Principle).
Чистая зависимость
Принцип «чистой зависимости» говорит о том, что более высокоуровневые модули не должны зависеть от низкоуровневых. Оба модуля должны зависеть от абстракций. Абстракции не зависят от деталей, а детали зависят от абстракций.
Use case
Принцип, определяющий входы и выходы пользовательских сценариев.
Приведем набившую оскомину диаграмму, на которой представлено более подробное описание слоев и направление связей.


Рис. № 2

Слева изображен общий взгляд на разделение по слоям:

  • Entities. Сущности, содержащие самую важную логику. Critical Business Rules – правила и законы, по которым работает система и без автоматизацииCritical Business Data – данные, необходимые для работы Critical Business RulesИ два этих понятия формируют понятие Entity. Entity – объект, который хранит в себе Critical Business Rules и необходимые для их работы данные (Critical Business Data);
  • Use Cases. Бизнес-процессы – объекты, манипулирующие Entity;
  • Controllers/Presenters/Gateways. Слой взаимодействия с внешним миром;
  • UI/DB/devices. Внешний мир (например, база данных).

На диаграмме представлены зависимости исходного кода, они идут от внешних слоёв к внутренним.
Справа показана диаграмма зависимостей:

  • Controller – уровень, с которым общаются внешние клиенты;
  • Use Case Input Port – интерфейс, представляющий бизнес-процессы;
  • Use Case Interactor – реализация Use Case Input Port;
  • Use Case Output Port – интерфейс для представления результатов бизнес-процесса;
  • Presenter – реализация Use Case Output Port.

Через всю диаграмму от Controller к Presenter идёт розовая стрелка – поток управления.
Пробуем применить эти данные к нашей задаче!

Пример реализации

Начнём с малого: пока ограничимся работой с деревом объектов и версиями изменения этого дерева (это наши Entity). Добавляем таблицы в БД. Важно понимать, что записи из dcc_objects не будут удаляться, чтобы по версиям можно было восстановить дерево на конкретную дату. Удаляемые объекты будут помечаться is_deleted = true.


Рис. № 3

Реализовывать будем с помощью .net core Web API. Приведём диаграмму архитектуры приложения ASP.NET Core в соответствии с принципами чистой архитектуры.


Рис. № 4

Создаём проект, разбиваем его на папки (соответствующие слоям) и добавляем соответствующие проекты.


Рис. № 5

Ядро (core):
  • Nicotech.Dcc.Domain. Слой Domain, в котором мы создаём наши Entity (DccTreeObj, DccTreeVersion, DccTreeChange), которые соответствуют таблицам созданным в БД. Никаких Critical Business Rules в них пока нет, только Critical Business Data.
  • Nicotech.Dcc.App. Слой Application, в котором реализованы Use case-ы. 


Рис. № 6

В отличие от Domain, где нет ничего кроме Critical Business Data, слой Application чуть интереснее.
  • Инверсия зависимостей
То, о чём мы говорили, обсуждая направление связей. Мы не должны засорять наше ядро, поэтому тут мы создаём интерфейс IDccDbContext, а реализация инфраструктурных зависимостей уже расположена в своём слое (папка Infrastructre).


  • Command and Query Responsibility Segregation (CQRS)

CQRS – архитектурный паттерн, при котором код, изменяющий состояние, отделяется от кода только читающего состояние. Одна из причин развития CQRS – несимметричное распределение нагрузки и сложности бизнес-логики на чтение и запись. Большинство бизнес-правил и валидаций располагается в write-подсистеме при этом читают данные, как правило, чаще. Подобное разделение позволяет по-разному подойти к оптимизации. В том числе могут быть использованы разные модели данных (оптимизированные под запись для команд и под чтение для запросов). Так же такая система может улучшить безопасность и упростить разработку. В проекте App есть папка CQRS, разбитая на папки по сущностям, которые в свою очередь разделены на команды и запросы. Это позволяет быстрее искать необходимую команду или запрос.

  • Медиатор

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

Рис. № 7

Итак, зачем нам медиатор и куда его встроить? С медиатором интерфейс ядра для UI слоя представляет из себя набор запросов и команд, которые обрабатывают хендлеры. В свою очередь каждый хендлер представляет отдельный use case. И располагать его нужно на границе между UI и Application.


Рис. № 8

Реализовывать отдельно шаблон своими силами – бесполезный труд, т.к. есть готовый Nuget-пакет MediatR. Всё. что нужно, это только реализовать интерфейс IRequest для команды/запроса и IRequestHandler для хендлера. Рассмотрим на примере команды создания новой версии.
Команда


Хендлер


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

Слой Infrastructure


Рис. № 9

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


DccTreeObjConf, DccTreeVersionConf, DccTreeChangeConf – классы, реализующие IEntityTypeConfiguration для каждой из трёх Entity, которые мы определили. Для удобства здесь же храним методы расширения для DI-контейнера.

Слой Presentation

В слое презентации также пока не так много интересного. Как уже говорилось раннее, мы расположили Mediator на границе UI и Application. Поэтому создадим базовый контроллер, в котором будет доступ к медиатору.


Теперь всё, что нам нужно, - это просто отправлять запросы/команды медиатору.


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

Базовые методы в эндпойнт добавлены, вернёмся к теме backend-a в статье про создание и распределение микрозадач.


Рис. № 10

Сергей Коновалов, главный разработчик ПО
Поиск по сайту