Исследование архитектурного подхода CQRS для создания бизнес - приложений

Создать новую статью

retweet
15.09.2010 10:00


Информация об авторе

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

Аннотация

Из статьи вы узнаете о попытке реализации движка системы автоматизации релиз - процесса программных продуктов (Software factory) с помощью архитектурного подхода CQRS.

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

Введение

Для автоматизации выпуска программных продуктов в Intel используется система автоматизации релиз - процесса выпуска продуктов под названием «Software factory».

релиз - процесс
Рис. 1 Релиз-процесс программного продукта

Задача (Task) выпуска программного продукта состоит из нескольких стадий (Stage): сбор исходников от разработчиков на репозиторий, создание инсталляционного пакета из исходников, проверка пакета. В итоге, при успешном последовательном прохождении всех стадий, мы получим программный продукт.

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

Основы CQRS

Архитектурный принцип CQRS (Command and Query Responsibility Segregation) является аналогом DDD (Domain-driven design) в распределенных системах. Он основывается на паттерне CQS (Command Query Separation).


Рис. 2 Приложение без использования паттерна CQS, с использованием CQS и с использованием CQRS

CQS заключается в следующем: любой метод в приложении является либо запросом (Query), возвращающим данные, либо командой (Command), изменяющей данные (вторая часть картинки). Рассмотрим пример кода (на языке C#):

// без использования паттерна CQS 
private int x; 
public int increment_and_return_x () 
{ 
    x = x + 1; 
    return x; 
}

// c использованием паттерна CQS 
private int x; 
public int value() 
{ 
    return x; 
} 
void increment_x() 
{ 
    x=x+1; 
}

В CQRS же этот паттерн выноситься на архитектурный уровень, практически разделяя приложение на 2 части (третья часть картинки).



Рис. 3 Структура приложения с использованием CQRS (упрощенная схема)

На схеме сразу же бросается в глаза 2 базы данных - это неспроста. Так как приложение фактически делится на 2 части, то каждая часть получает свою базу данных, оптимизированную под ее нужды. Одна база данных, оптимизированная на запись (в нее удобно сохранять данные), она хранит события (Events) в бинарном виде. Для нее удобно использовать объектную базу данных. Другая база - на чтение (данные в ней подготовлены к отображению пользователю и есть оптимизации для поиска), в ней фактически есть под каждую форму приложения есть своя таблица. Для этой базы удобно применять реляционные СУБД.

Рассмотрим схему на примере бронирования билета в кино через web - сайт. Допустим, клиент выбрал место и сеанс и нажал кнопку «забронировать билет». Таким образом, будет сформирована команда «забронировать билет», содержащая информацию о клиенте (Ф.И.О.) и билете (фильм, место и время). Эта команда через слой инфраструктурных классов «Command infrastructure» попадет в доменный объект, отвечающий за бронирование билетов. Доменный объект проверит: возможно ли забронировать данный билет, и если все хорошо, то он сгенерирует событие (event) о том, что билет забронирован. Но как вы заметили, никаких изменений в данных пока не произойдет! Событие будет содержать все необходимые для бронирования билета данные. Когда событие вернется в доменный объект, только тогда он изменит свое состояние (это паттерн event sourcing, реализация системы без этого паттерна рассматриваться не будет). Так же событие попадает в write - базу и в денормализатор. Денормализатор удалит забронированное место из таблицы доступных мест и добавит в таблицу забронированных в read - базе.

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

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

Углубляемся в CQRS

Теперь рассмотрим более подробно схему приложения и реализацию от начала до конца одной команды. За основу был взят фреймворк Fohjin, разработанный Mark Nijhof. На схеме отмечено зеленым - блоки пользователем фреймворка, а синим - реализованные в фреймворке Fohjin



Рис. 4 Структура приложения с использованием CQRS

Первым делом мы рассмотрим добавление задачи (Task). Каждая задача характеризуется именем и последовательно выполняющимися стадиями. При добавлении задачи автоматически должна добавляться стадия создания пакета (в данном случае Packaging stage). Таким образом, на стороне клиента происходит следующее:

private void AddNewTask() 
{ 
    // Создаем команду, забирая имя задачи из интерфейса и добавляя стадию 
    var command = new AddNewTaskCommand 
    { 
        TaskName = view.TaskName, 
        Stages = new Stage[] {new PackagingStage()} 
    }; 
    // Отдаем команду WCF сервису для выполнения
    service.ExecuteCommand(command); 
}

Дальше команда через Command bus (узкое место, гарантирующее последовательно выполнение только одной команды) попадает в Command handler. Command handler - это класс, принимающий команду строго определенного типа и вызывающий у доменных объектов соответствующие методы. Команда связывается с Command handler’ом наследованием Command handler’а от интерфейса, параметризированного типом команды:

class AddNewTaskCommandHandler : ICommandHandler<AddNewTaskCommand>

Интерфейс ICommandHandler заставляет нас реализовать метод Execute, который принимает команду и оперирует доменными объектами через репозиторий:

public void Execute(AddNewTaskCommand command) 
{ 
    var task = new Task(command.TaskName, command.Stages);
    repository.Add(task); 
}

Весь Command handler выглядит следующим образом:

public class AddNewTaskCommandHandler : ICommandHandler<AddNewTaskCommand> 
{ 
    private readonly IDomainRepository<IDomainEvent> repository; 
    // Получаем в конструкторе репозиторий задач и сохраняем ссылку на него 
    public AddNewTaskCommandHandler(IDomainRepository<IDomainEvent> repository) 
    { 
        this.repository = repository; 
    } 
    public void Execute(AddNewTaskCommand command) 
    { 
        // Создаем задачу из данных, пришедших в команде 
        var task = new Task(command.TaskName, command.TaskStartCondition, command.Stages); 
        // Добавляем команду в репозиторий repository.Add(task); 
    } 
}

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

public Task(string taskName, IEnumerable<Stage> stages) 
{ 
    // Инициализируем контейнер стадий 
    Stages = new EntityList<Stage, IDomainEvent>(this); 
    // Регистрируем обработчики событий 
    RegisterEvent<NewTaskAddedEvent>(OnNewTaskAdded); 
    RegisterEvent<StageAddedEvent>(OnStageAdded); 
    // Создаем событие var newTaskAddedEvent = new NewTaskAddedEvent(Guid.NewGuid(), taskName, stages); 
    // Порождаем событие Apply(newTaskAddedEvent); 
}

В обработчике события добавления задачи мы должны сохранить информацию из события в доменный объект (в задачу) и сгенерировать событие добавления нужного количества новых стадий. Как вы заметили, стадия добавляется по событию, это сделано для того, чтобы в Event handler’е (см. ниже) не дублировать логику доменного объекта. Код обработчика:

protected void OnNewTaskAdded(NewTaskAddedEvent theEvent) 
{ 
    // Сохраняем данные из события 
    Id = theEvent.TaskId; 
    Name = theEvent.TaskName; 
    Status = TaskStatus.NotStarted; 
    Result = ExecutionResult.NoResult; 
    StartCondition = theEvent.StartCondition; 
    // Генерируем события на добавление стадий 
    int stageNumber = Stages.Count; 
    foreach (var stage in theEvent.Stages) 
    { 
        Apply(new StageAddedEvent(Id, Name, Status, stage, stageNumber)); 
    } 
}

Обработчик события добавления стадии просто добавляет в стадию список стадий:

protected void OnStageAdded(StageAddedEvent theEvent) 
{ 
    Stages.Add(theEvent.Stage); 
}

Но вернемся к первому событию (добавление задачи). Согласно схеме, событие не только возвращается в доменный объект (через Event publisher), но и попадает в Event store (та самая write - база) и в Event handler. Event handler связан с Event аналогичным образом с Command и Command handler – через параметризированный интерфейс:

public class NewTaskAddedEventHandler : IEventHandler<NewTaskAddedEvent>

Этот интерфейс требует реализовать метод Execute, принимающий наше событие. В этом методе мы должны денормализовать данные (при необходимости) и разложить их по нужным репозиториям. Нам нужно добавить запись в репозиторий «общий список задач» и в репозиторий не запущенных задач:

public class NewTaskAddedEventHandler : IEventHandler<NewTaskAddedEvent> 
{ 
    … 
    public void Execute(NewTaskAddedEvent theEvent) 
    { 
        var tasksReport = new TasksReport 
            {
                Id = theEvent.TaskId, 
                TaskName = theEvent.TaskName, 
                TaskStatus = TaskStatus.NotStarted
            }; 
        taskRepository.Save(tasksReport); 
        var notStartedTasksReport = new NotStartedTasksReport 
            {
                Id = theEvent.TaskId, 
                TaskName = theEvent.TaskName
            }; 
        notStartedTasksRepository.Save(notStartedTasksReport); 
    } 
}

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

Хотелось бы обратить внимание на Event store - как вы заметили, в нем мы храним события. Если для их хранения мы используем реляционную базу данных, то приходится их хранить в бинарном виде, что не очень удобно. Поэтому это замечательный способ избавиться от реляционной БД и использовать объектно - ориентированную.

А теперь кратко о фреймворке тестирования. Для тестирования применяются Behaivior - тесты (почитать можно здесь, здесь и здесь). Автор фреймворка предоставляет базовые классы для тестирования. Общий процесс выглядит следующим образом:

  1. Условие (Given): накатываем на систему события, чтобы привести ее в нужное нам состояние.
  2. Событие (When): отсылаем в систему проверяемую команду
  3. Результат (Then): проверяем сгенерившиеся события после выполнения команды и изменения в репозиториях (read - база).

 

Что сразу бросается в глаза – это органичность тестов. Программист почти не сталкивается с фейковыми классами и моками, а оперирует теми же объектами, что и сама система.

Заключение

В рамках «Летней школы Intel 2010» был реализован движок системы автоматизации релиз процесса Software factory, позволяющий очень просто масштабировать систему. Доказана применимость данного архитектурного подхода. В дальнейшем эту систему будут продолжать разрабатывать сотрудники Intel и, конечно же, она будет внедрена.

«Летняя школа Intel» оправдала мои ожидания, эти 2 месяца были просто фантастическими, такого лета у меня еще никогда еще не было. Наверное, никому из «летних школьников» не хотелось уезжать. Поразила слаженность работы такой огромной компании, после стажировки работа университета только огорчает. Было интересно наблюдать, как профессионалы своего дела работают вместе и как менеджер координирует их действия. Отвыкнуть от всего этого очень сложно!

За это лето я ощутил полезность парного программирования, инструментов для рефакторинга. Как всегда удались мастер - классы от Антона Бевзюка и Евгения Сорокина, но в этом году они (мастер - классы) были слишком поздно (из - за отпусков), поэтому все приходилось постигать в парном программировании с ментором. Поэтому хочу сказать спасибо моему ментору Ксении Мухортовой, Дмитрию Петухову, а так же менеджеру Наталии Муравьевой и всем членам команды Automation Engineering.

Список использованных материалов

DDD:
wikipedia.org

CQS:
wikipedia.org

CQRS:
elegantcode.com

Фреймворк Fohjin:
elegantcode.com

BDD:
habrahabr.ru ibm.com

Event sourcing:
codebetter.com