Архитектурные паттерны

Onion architecture / Hexagonal Architecture

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

Ниже пример "классического"(это не значит что он является единственно верным) способа разделения кода приложения на слои:

Domain layer

Domain layer — модель бизнес-логики приложения. В идеале вся бизнес логика(понятия и операции которым оперирует бизнес) должна находиться в этом слое. Задача остальных слоев, это инкапсуляция бизнес логики от объектов реального мира(бд, сети, файлов, пользователей, и т.д).

Application layer

Слой ответственный за "связь" доменной модели и инфраструктурными сервисами. Никакие другие классы, кроме классов данного слоя не могут дергать объекты доменного. В терминологии Фаулера, называется service layer.

Infrastructure layer

Инфраструктурный слой, содержащий всё необходимое для общения приложения с внешним миром(пользователями, сторонними сервисами, железом и т.д). быстро может стать очень жирным. Как я уже говорил, обычно, этот код сложен и нестабилен. Инфраструктурный код соединяет ядро нашего драгоценного приложения с:

  • Файловой системой
  • Сетью
  • Орм
  • Фреймворком
  • Сторонними библиотеками

Очень важно понимать что здесь не может быть НИКАКОЙ бизнес логики.

Presentation layer

По сути, представляет собой подмножество слоя Infrastructure для работы с пользовательским вводом/выводом. Многие не выделяют этот слой в отдельный. Содержит в себе веб контроллеры, вьюхи, обработчики консольных команд и т.д

Command and Query Responsibility Segregation (CQRS)

CQRS – подход проектирования программного обеспечения, при котором код, изменяющий состояние, отделяется от кода, просто читающего это состояние. Подобное разделение может быть логическим и основываться на разных уровнях. Кроме того, оно может быть физическим и включать разные звенья (tiers), или уровни.

В основе этого подхода лежит принцип Command-query separation (CQS). Основная идея CQS в том, что в объекте методы могут быть двух типов:

  • Commands: Методы изменяют состояние объекта, не возвращая значение.
  • Queries: Методы возвращают результат, не изменяя состояние объекта. Другими словами, у Query не никаких побочных эффектов.

Event Driven

Архитектура, управляемая событиями (event-driven architecture, EDA) архитектура, в основе которой лежит создание, определение, потребление и реакции на события. Т.е любое изменение в системы должно выбрасывать событие, на которое могут реагировать другие части системы.

Event Sourcing

Идея Event sourcing (ES)заключается в том, что любому изменению модели можно сопоставить какое-то бизнес-событие, и сохранение всех событий является достаточным для того, чтобы каждый раз заново воспроизвести то же состояние модели.

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

Из-за ряда особенностей данного подхода использовать его повсеместно не получится. Event Sourcing не является серебряной пулей. Его использование оправдано там, где есть вероятность множества изменений, которые надо как-то контролировать.

Поток событий

Поток событий - это упорядоченный список событий, которые были применены в рамках агерата. Каждое новое событие увеличивает версию потока на 1.

Проблемы

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

Снимки (Snapshot)

Для решения проблемы, связанной с необходимостью накладывать множество событий на агрегат используются снимки. Снимок - сериализованное представление агрегата какой-либо версии (например, 10) Когда в следующий раз мы захотим получить текущее состояние агрегата для версии 20, нам необязательно накладывать все предшествующие 20 событий. Достаточно получить снимок 10-ой версии и применить к нему недостающие события (т.е. ещё 10)

Представления (Projections)

Эффективная работа с Event Sourcing предполагает разделение на 2 интерфейса: write model (наш агрегат) и read model (представление). Представление - это то, с чем будут работать клиенты (например, через API). Оно формируется на основании изменений и в том виде, в котором необходимо. По сути представление - это просто ключ и набор данных, которые были собраны специально под тип запроса. Данный подход позволяет полностью исключить из работы все запросы с соединениями, группировками и т.д., ибо данные уже сохранены в том виде, в котором необходимы для использования.

Индексы

Для решения проблемы, связанной с фильтрацией данных, можно взять любое key\value хранилище для реализации маппинга. Например, нам необходимо обеспечить уникальность email пользователя. В классической Event Sourcing имплементации это если и возможно, то весьма затратно. Но можно поступить иначе: когда мы создаём пользователя, мы записываем его идентификатор и email в специальное хранилище. Когда мы будем создавать другого пользователя, мы можем проверить, используется ли у кого-либо данный email, или нет

Saga

Шаблон “Saga” используется для моделирования long-running (Как это будет по-русски? Долгосрочные? Долгоиграющие?) бизнес-процессов. Фактически, мы можем сказать, что Saga представляет собой Workflow для какого-то определённого сценария.

Long-running не следует понимать в терминах секундной стрелки и задаваться вопросом: 200 миллисекунд – это long-running или нет? В системах с архитектурой, построенной на событиях и сообщениях подобные вопросы вряд ли имеют смысл.

Идея, которую реализует шаблон “Saga” проста: после каждого успешно выполненного шага мы имеем некоторое состояние, с которого можно будет продолжить исполнение процесса. Шагом является выполнение какого-то действия, реакция на какое-то событие и т.д. То есть, если мы не смогли подтвердить транзакцию в базу данных, или если вызов второго веб-сервиса завершился неудачей, у нас имеется состояние, валидное на момент до его вызова. Наш бизнес-процесс остановлен – это да, но он и не потерян. Мы можем предпринять какие-то действия и продолжить процесс. Как результат – процесс просто выполнялся дольше.

Кроме того, имея такое состояние, мы можем легко моделировать процессы, “управляемые” событиями!

Inversion of Control, Dependency Inversion, Dependency Injection

Инверсия управления (IoC, Inversion of Control) – это достаточно общее понятие, которое отличает библиотеку от фреймворка. Классическая модель подразумевает, что вызывающий код контролирует внешнее окружение и время и порядок вызова библиотечных методов. Однако в случае фреймворка обязанности меняются местами: фреймворк предоставляет некоторые точки расширения, через которые он вызывает определенные методы пользовательского кода.

Инверсия управления в ООП, иначе инверсия зависимостей(DI, dependency inversion)— важный принцип объектно-ориентированного программирования, используемый для уменьшения coupling в компьютерных программах. Есть два паттерна реализации DI:

  • Суть паттерна Service Locator сводится к тому, что вместо создания конкретных объектов («сервисов») напрямую с помощью ключевого слова new, мы будем использовать специальный «фабричный» объект, который будет отвечать за инициализацию и предоставление всех сервисов.
  • Внедрение зависимости (Dependency injection, DI) — Внедрение зависимостей (DI, Dependency Injection) – это механизм передачи классу его зависимостей. Существует несколько конкретных видов или паттернов внедрения зависимостей: внедрение зависимости через конструктор (Constructor Injection), через метод (Method Injection) и через свойство (Property Injection). В полном соответствии с принципом единой обязанности объект отдаёт заботу о построении требуемых ему зависимостей внешнему, специально предназначенному для этого общему механизму, и не дергает никаких контейнеров/фабрик/реестров для получения нужных сервисов самостоятельно(в этом ключевое отличие от Service Locator).

Dependency Inversion Principe(DIP)

Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те, и другие должны зависеть от абстракции. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.