Как стать автором
Обновить

CQRS и микросервисы в продуктовой разработке

Время на прочтение11 мин
Количество просмотров8.6K

Как спроектировать продукт, чтобы не зарыть деньги в землю


На каком этапе создания продукта или системы подключать архитектурное проектирование системы, чтобы потом не было мучительно больно за потраченные деньги? Как решить, совмещать ли CQRS и микросервисы.


Эта статья для представителей бизнеса с запросом на разработку ИТ-решения. Мы подскажем, как запустить продукт и избежать неоправданных затрат, связанных с архитектурой. А также посмотрим, как использование CQRS поможет при реализации функционала в разных клиентах приложения, и являются ли микросервисы той самой панацеей.


Коротко о CQRS


CQRS (Command-Query Responsibility Segregation) — шаблон, применяемый при разработке систем, который гласит, что любой метод системы может быть либо запросом (не изменяющим состояние системы), либо командой (изменяющим состояние системы). Как показывает практика, это один из самых часто применяемых шаблонов при разработке ПО. Его можно применять на разных уровнях и в различных ситуациях. Например, классическое разделение систем на OLTP/OLAP, когда данные пишутся часто в OLTP-систему, а читаются из OLAP-системы, является ни чем иным как применением шаблона CQRS в архитектуре БД.



В “древние” времена (начало 2000 годов) популярные системы подталкивали к применению CQRS. Например, при использовании Interbase/FirebirdSQL рекомендуется использовать разные типы транзакций для чтения и записи. В современном мире очень часто встречается случай сосуществования двух систем на разных уровнях архитектуры. Например, может быть разделение на уровне различных систем, когда личный кабинет клиента на сайте реализует только Query-функциональность, а все изменения происходят в CRM-системе внутри компании через заранее определенные интерфейсы Command. Можно найти примеры использования CQRS на уровне архитектуры JS приложения. Кто бы мог подумать несколько лет назад, что слова архитектура и JS будут использоваться в одном приложении… Хотя, возможно, это излишний стеб.


Две крайности при разработке


Типичная ситуация: A Big Ball of Mud



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


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


В таких ситуациях от основателей и стейкхолдеров бизнеса можно услышать фразы: “Нам бы побыстрее выйти в релиз и получить первых клиентов. А потом уже давайте думать, что делать с клиентами и как улучшать наш продукт”. При таком подходе, когда случается реальный наплыв первых клиентов, система начинает валиться, и вся команда начинает тушить пожары. В такой ситуации иллюзия “хорошей жизни” внезапно развеивается. Разработчики и все держатели контекста постепенно покидают команду. Клиенты недовольны. В результате бизнес рушится из-за того, что не смог масштабироваться.


Микросервисы не спасут


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


При разделении системы на микросервисы возникает много вопросов по взаимодействию систем. Например, как отследить цепочку вызовов различных сервисов, которая привела к конкретной ошибке. Или как понять, какой сервис сейчас является узким местом в работе системы. Эти вопросы с разной степенью успешности решаются либо готовыми инфраструктурными решениями (Elastic для логгирования), либо под них надо разрабатывать какие-то свои инфраструктурные сервисы. К примеру, балансировщик, который учитывает особенности бизнес-логики при маршрутизации запросов. Эти проблемы характерны не только для микросервисных систем, но и для всех распределенных систем.


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


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


Золотая середина


Продуманная архитектура — залог успеха


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


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


Пример


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


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


Так с чего начать?


На практике для выработки таких архитектурных решений хорошо помогает одно- или двухдневный воркшоп с заказчиком, на котором прорабатывается не только архитектура решения, но и low-fidelity дизайн системы и основные сценарии использования. В случае со стартапами чрезвычайно важным шагом является проработка Business Canvas вместе с заказчиком (если его еще нет) для того, чтобы все стороны поняли жизнеспособность идеи. Не исключено, что результатом будет закрытие проекта сразу после воркшопа: он поможет стейкхолдерам увидеть несостоятельность бизнес-замысла, не потратив время и деньги на техническую реализацию. Как бы странно не звучало, даже в таких ситуация очень сильно возрастает доверие между участниками проекта. Одним из результатов воркшопа будет являться документ, описывающий нефункциональные требования к разрабатываемой системе. Резонный вопрос — надо ли это делать и дорого ли это? Отвечаем: это стоит 2 дня плотной работы организованной команды. Большинство ошибок при разработке продукта, если этого не сделать, будут стоить намного дороже.


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


Что сделали мы


В одном из проектов по управлению и автоматизации бизнес-процессов компании с флотом морских судов перед нами стояла задача в минимальные сроки и с минимальным затратами выпустить первую версию ПО. Был выбран подход внедрения CQRS на уровне логической архитектуры.


Само приложение имело следующую схему архитектуры:



Как видно из схемы, если соблюдать базовые принципы SOLID, а именно Dependency Inversion и Dependency Injection, то при хорошей настройке контейнеров инверсии управления все команды и запросы становятся небольшими кусочками, которые очень легко начать переиспользовать в разных частях вашей системы. Так и было в этом случае, что дало свой положительный результат.


В данном случае видно 3 части системы, которые вполне могут иметь схожую работу с Моделью:


  1. Взаимодействие Офисного приложения с моделью (Admin office Desktop App)
  2. Взаимодействие приложения Механика с моделью (Ship Desktop Mechanic app)
  3. Взаимодействие сервиса синхронизации с моделью (SyncService)

Было замечено, что люди, которые используют приложение в офисе, намного чаще читают данные с кораблей, нежелие заполняют. При этом люди, которые находятся в Offline- зоне на кораблях, наоборот больше записывают в приложение данные, чем читают. Зачем это нужно знать? Затем, что пока это лишь заготовки под масштабирование архитектуры, которые конечно уже принесли свою пользу, но могут принести гораздо больше пользы в следующих этапах развития архитектуры. При желании выделить окружающие контексты из кусочков запросов в одни физические сервисы с оптимизированной базой данных под чтение будет намного легче, а контексты из команд — в другие сервисы. Всё это уже будет зависеть от бизнес-задач и от направлений развития архитектуры.


Примеры того, как внедрить такое разбиение у себя в проекте


Ниже я привожу пару основных классов на языке C#, которые можно просто переиспользовать в своих решениях.


/// <summary>
///   Универсальная фабрика для создания запросов.
///   Должна быть реализована в Composition Root(web api проект, главный проект сайта и т.д.)
/// </summary>
    public interface IHandlersFactory
    {
        IQueryHandler<TQuery, TResult> CreateQueryHandler<TQuery, TResult>();

        IAsyncQueryHandler<TQuery, TResult> CreateAsyncQueryHandler<TQuery, TResult>();

        ICommandHandler<TCommand> CreateCommandHandler<TCommand>();

        IAsyncCommandHandler<TCommand> CreateAsyncCommandHandler<TCommand>();
    }
/// <summary>
///   Базовый интерфейс для выполнения команды
/// </summary>
 public interface ICommandHandler<TCommand>
    {
        void Execute(TCommand command);
    }
/// <summary>
///   Базовый интерфейс для выполнения запроса
/// </summary>

public interface IQueryHandler<TQuery, TResult>
    {
        TResult Execute(TQuery query);
    }

/// <summary>
///   Фабрика для Ninject, создающая типизированные команды и запросы
/// </summary>
public class NinjectFactory : IHandlersFactory
    {
        private readonly IResolutionRoot _resolutionRoot;

        public NinjectFactory(IResolutionRoot resolutionRoot)
        {
            _resolutionRoot = resolutionRoot;
        }

        public IAsyncCommandHandler<TCommand> CreateAsyncCommandHandler<TCommand>()
        {
            return _resolutionRoot.Get<IAsyncCommandHandler<TCommand>>();
        }

        public IAsyncQueryHandler<TQuery, TResult> CreateAsyncQueryHandler<TQuery, TResult>()
        {
            return _resolutionRoot.Get<IAsyncQueryHandler<TQuery, TResult>>();
        }

        public ICommandHandler<TCommand> CreateCommandHandler<TCommand>()
        {
            return _resolutionRoot.Get<ICommandHandler<TCommand>>();
        }

        public IQueryHandler<TQuery, TResult> CreateQueryHandler<TQuery, TResult>()
        {
            return _resolutionRoot.Get<IQueryHandler<TQuery, TResult>>();
        }
    }

Пример Биндинга запросов через Ninject


public override void Load()
        {
            // queries
            Bind<IQueryHandler<GetCertificateByIdQuery, Certificate>>().To<GetCertificateByIdQueryHandler>();
            Bind<IQueryHandler<GetCertificatesQuery, List<Certificate>>>().To<GetCertificatesQueryHandler>();
            Bind<IQueryHandler<GetCertificateByShipQuery, List<Certificate>>>().To<GetCertificateByShipQueryHandler>();
…………
}

После инъекции IHandlerFactory в ваш класс вы получаете возможность использовать свои команды и запросы следующим образом:


Пример выполнения запроса:


Ship ship = mHandlersFactory.CreateQueryHandler<GetShipByIdQuery, Ship>().Execute(new GetShipByIdQuery(id));

Пример выполнения команды:


mHandlersFactory.CreateCommandHandler<DeleteReportCommand>()
                    .Execute(new DeleteReportCommand(report));

Когда я только начал пользоваться этими наработками, мысли были такие:


  1. Блин, я раньше писал функции в репозитории, а теперь мне надо делать классы почти из каждой функции.
  2. Оказывается создание классов можно автоматизировать, да и вообще это не проблема, и времени занимает только на пару секунд больше.
  3. Офигеть, теперь можно найти нужное мне место в коде в несколько раз быстрее. Я просто знаю как организованы папки из контекстов и команд/запросов, и мне вообще не надо искать ничего. Я просто открываю нужный мне класс.
  4. Круто, это ещё можно и переиспользовать!

Но конечно всё надо применять ситуативно и с умом. Для этого и нужно разработать архитектуру. Другой момент что не надо её пытаться придумать на 300 шагов вперёд. Она должна ситуативно развиваться вместе с продуктом, и быть всем ответом минимум на 3 вопроса:


  1. Почему структура моего ИТ-продукта именно такая? Почему в этом месте надо реализовывать именно так?
  2. Если я придумаю крутую реализацию, то как она ляжет на общую картинку системы?
  3. Каким образом будут соблюдаться нефункциональные требования системы в целом?

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


Преимущества CQRS на уровне логической архитектуры


Базовая архитектура рассмотренного приложения была выстроена с соблюдением разных принципов и шаблонов проектирования: MVVM, SOLID, CQRS и т.д. Это позволило переиспользовать функциональность фич для разных клиентов приложения. При этом, внедрение не занимало много времени и было достаточно недорогим.


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


Напоследок


Ошибочно Agile-подход к разработке трактуется как “не надо ничего проектировать заранее, надо ввязаться в бой, а там война — план покажет”. А если нам будет не хватать скорости работы или скорости изменения программы, мы зарядим серебряную пулю — микросервисы. Ведь на всех конференциях рассказывают, что микросервисы — это просто и решает сразу все проблемы. Такой оптимизм, как правило, ведет к потере денег и неработающему продукту.


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

Теги:
Хабы:
Всего голосов 12: ↑1 и ↓11-10
Комментарии12

Публикации

Истории

Работа

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань