Рубрики
Без рубрики

CQR и поиск событий в Java

Изучите основные концепции шаблонов проектирования CQR и источников событий.

Автор оригинала: Kumar Chandrakant.

1. введение

В этом руководстве мы рассмотрим основные концепции разделения ответственности за командные запросы (CQR) и шаблоны проектирования источников событий.

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

2. Основные Понятия

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

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

2.1. Источник событий

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

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

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

2.2. CQRS

Проще говоря, CQRS-это разделение команд и запросов на стороне архитектуры приложения . CQRS основан на принципе разделения командных запросов (CQS), который был предложен Бертраном Мейером. CQS предлагает разделить операции с объектами домена на две отдельные категории: Запросы и команды:

Запросы возвращают результат и не изменяют наблюдаемое состояние системы. Команды изменяют состояние системы, но не обязательно возвращают значение .

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

3. Простое Приложение

Мы начнем с описания простого приложения на Java, которое строит модель домена.

Приложение будет предлагать операции CRUD для модели домена , а также будет иметь сохраняемость для объектов домена. CRUD означает Создание, чтение, Обновление и Удаление, которые являются основными операциями, которые мы можем выполнять над объектом домена.

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

В этом процессе мы будем использовать некоторые концепции доменного проектирования (DDD) в нашем примере.

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

3.1. Обзор приложения

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

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

3.2. Реализация приложения

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

public class User {
private String userid;
    private String firstName;
    private String lastName;
    private Set contacts;
    private Set
addresses; // getters and setters } public class Contact { private String type; private String detail; // getters and setters } public class Address { private String city; private String state; private String postcode; // getters and setters }

Кроме того, мы определим простой репозиторий в памяти для сохранения состояния нашего приложения. Конечно, это не добавляет никакой ценности, но достаточно для нашей демонстрации позже:

public class UserRepository {
    private Map store = new HashMap<>();
}

Теперь мы определим службу для предоставления типичных операций CRUD в нашей модели домена:

public class UserService {
    private UserRepository repository;
    public UserService(UserRepository repository) {
        this.repository = repository;
    }

    public void createUser(String userId, String firstName, String lastName) {
        User user = new User(userId, firstName, lastName);
        repository.addUser(userId, user);
    }

    public void updateUser(String userId, Set contacts, Set
addresses) { User user = repository.getUser(userId); user.setContacts(contacts); user.setAddresses(addresses); repository.addUser(userId, user); } public Set getContactByType(String userId, String contactType) { User user = repository.getUser(userId); Set contacts = user.getContacts(); return contacts.stream() .filter(c -> c.getType().equals(contactType)) .collect(Collectors.toSet()); } public Set
getAddressByRegion(String userId, String state) { User user = repository.getUser(userId); Set
addresses = user.getAddresses(); return addresses.stream() .filter(a -> a.getState().equals(state)) .collect(Collectors.toSet()); } }

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

3.3. Проблемы в этом приложении

Прежде чем мы продолжим наше обсуждение с Event Sourcing и CQR, стоит обсудить проблемы с текущим решением. В конце концов, мы будем решать те же проблемы, применяя эти шаблоны!

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

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

4. Введение CQRS

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

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

  • Агрегат/Агрегатор :

Aggregate-это шаблон, описанный в Domain-Driven Design (DDD), который логически группирует различные сущности путем привязки сущностей к корню aggregate . Совокупный шаблон обеспечивает согласованность транзакций между сущностями.

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

  • Проекция/Проектор :

Проекция-еще один важный паттерн, который значительно улучшает CQR. Проекция по существу означает представление объектов предметной области в различных формах и структурах .

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

4.1. Реализация стороны записи приложения

Давайте сначала реализуем сторону записи приложения.

Мы начнем с определения необходимых команд. Команда – это намерение изменить состояние модели домена . Успех или неудача зависит от бизнес-правил, которые мы настраиваем.

Давайте посмотрим наши команды:

public class CreateUserCommand {
    private String userId;
    private String firstName;
    private String lastName;
}

public class UpdateUserCommand {
    private String userId;
    private Set
addresses; private Set contacts; }

Это довольно простые классы, которые содержат данные, которые мы намерены мутировать.

Затем мы определяем агрегат, который отвечает за прием команд и их обработку. Агрегаты могут принимать или отклонять команду:

public class UserAggregate {
    private UserWriteRepository writeRepository;
    public UserAggregate(UserWriteRepository repository) {
        this.writeRepository = repository;
    }

    public User handleCreateUserCommand(CreateUserCommand command) {
        User user = new User(command.getUserId(), command.getFirstName(), command.getLastName());
        writeRepository.addUser(user.getUserid(), user);
        return user;
    }

    public User handleUpdateUserCommand(UpdateUserCommand command) {
        User user = writeRepository.getUser(command.getUserId());
        user.setAddresses(command.getAddresses());
        user.setContacts(command.getContacts());
        writeRepository.addUser(user.getUserid(), user);
        return user;
    }
}

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

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

public class UserWriteRepository {
    private Map store = new HashMap<>();
    // accessors and mutators
}

На этом завершается запись нашего приложения.

4.2. Реализация стороны чтения приложения

Теперь давайте перейдем на сторону чтения приложения. Мы начнем с определения стороны чтения модели предметной области:

public class UserAddress {
    private Map> addressByRegion = new HashMap<>();
}

public class UserContact {
    private Map> contactByType = new HashMap<>();
}

Если мы вспомним наши операции чтения, нетрудно увидеть, что эти классы прекрасно справляются с ними. В этом и заключается прелесть создания модели домена, ориентированной на запросы, которые у нас есть.

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

public class UserReadRepository {
    private Map userAddress = new HashMap<>();
    private Map userContact = new HashMap<>();
    // accessors and mutators
}

Теперь мы определим необходимые запросы, которые мы должны поддерживать. Запрос — это намерение получить данные-он не обязательно может привести к получению данных.

Давайте посмотрим наши запросы:

public class ContactByTypeQuery {
    private String userId;
    private String contactType;
}

public class AddressByRegionQuery {
    private String userId;
    private String state;
}

Опять же, это простые классы Java, содержащие данные для определения запроса.

Что нам сейчас нужно, так это проекция, которая может обрабатывать эти запросы:

public class UserProjection {
    private UserReadRepository readRepository;
    public UserProjection(UserReadRepository readRepository) {
        this.readRepository = readRepository;
    }

    public Set handle(ContactByTypeQuery query) {
        UserContact userContact = readRepository.getUserContact(query.getUserId());
        return userContact.getContactByType()
          .get(query.getContactType());
    }

    public Set
handle(AddressByRegionQuery query) { UserAddress userAddress = readRepository.getUserAddress(query.getUserId()); return userAddress.getAddressByRegion() .get(query.getState()); } }

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

4.3. Синхронизация данных чтения и записи

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

Именно здесь нам понадобится нечто, известное как проектор. Проектор имеет логику для проецирования модели домена записи в модель домена чтения .

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

public class UserProjector {
    UserReadRepository readRepository = new UserReadRepository();
    public UserProjector(UserReadRepository readRepository) {
        this.readRepository = readRepository;
    }

    public void project(User user) {
        UserContact userContact = Optional.ofNullable(
          readRepository.getUserContact(user.getUserid()))
            .orElse(new UserContact());
        Map> contactByType = new HashMap<>();
        for (Contact contact : user.getContacts()) {
            Set contacts = Optional.ofNullable(
              contactByType.get(contact.getType()))
                .orElse(new HashSet<>());
            contacts.add(contact);
            contactByType.put(contact.getType(), contacts);
        }
        userContact.setContactByType(contactByType);
        readRepository.addUserContact(user.getUserid(), userContact);

        UserAddress userAddress = Optional.ofNullable(
          readRepository.getUserAddress(user.getUserid()))
            .orElse(new UserAddress());
        Map> addressByRegion = new HashMap<>();
        for (Address address : user.getAddresses()) {
            Set
addresses = Optional.ofNullable( addressByRegion.get(address.getState())) .orElse(new HashSet<>()); addresses.add(address); addressByRegion.put(address.getState(), addresses); } userAddress.setAddressByRegion(addressByRegion); readRepository.addUserAddress(user.getUserid(), userAddress); } }

Это довольно очень грубый способ сделать это, но дает нам достаточно понимания того, что необходимо для работы CQR. Более того, нет необходимости, чтобы репозитории чтения и записи находились в разных физических хранилищах. Распределенная система имеет свою долю проблем!

Обратите внимание, что не удобно проецировать текущее состояние домена записи в различные модели домена чтения . Пример, который мы привели здесь, довольно прост, поэтому мы не видим проблемы.

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

4.4. Преимущества и недостатки CQRS

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

Давайте теперь обсудим некоторые другие преимущества, которые CQRS привносит в архитектуру приложений:

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

Однако это не бесплатно. Как видно из этого простого примера, CQRS значительно усложняет архитектуру. Это может быть не подходит или не стоит боли во многих сценариях:

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

5. Внедрение Источников событий

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

Мы представим источник событий для решения этой проблемы. Источник событий резко меняет наше представление о хранилище состояний приложений .

Давайте посмотрим, как это изменит наш репозиторий:

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

5.1. Реализация событий и Хранилище событий

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

public abstract class Event {
    public final UUID id = UUID.randomUUID();
    public final Date created = new Date();
}

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

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

Далее, давайте создадим некоторые доменные события, наследуемые от этого базового события:

public class UserCreatedEvent extends Event {
    private String userId;
    private String firstName;
    private String lastName;
}

public class UserContactAddedEvent extends Event {
    private String contactType;
    private String contactDetails;
}

public class UserContactRemovedEvent extends Event {
    private String contactType;
    private String contactDetails;
}

public class UserAddressAddedEvent extends Event {
    private String city;
    private String state;
    private String postCode;
}

public class UserAddressRemovedEvent extends Event {
    private String city;
    private String state;
    private String postCode;
}

Это простые POJO на Java, содержащие сведения о событии домена. Однако здесь важно отметить детализацию событий.

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

Теперь, естественно, нам нужен репозиторий для хранения событий нашего домена:

public class EventStore {
    private Map> store = new HashMap<>();
}

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

5.2. Генерация и потребление событий

Итак, теперь наш сервис, который обрабатывал все операции CRUD, изменится. Теперь вместо обновления состояния движущегося домена он будет добавлять события домена. Он также будет использовать те же события домена для ответа на запросы.

Давайте посмотрим, как мы можем этого достичь:

public class UserService {
    private EventStore repository;
    public UserService(EventStore repository) {
        this.repository = repository;
    }

    public void createUser(String userId, String firstName, String lastName) {
        repository.addEvent(userId, new UserCreatedEvent(userId, firstName, lastName));
    }

    public void updateUser(String userId, Set contacts, Set
addresses) { User user = UserUtility.recreateUserState(repository, userId); user.getContacts().stream() .filter(c -> !contacts.contains(c)) .forEach(c -> repository.addEvent( userId, new UserContactRemovedEvent(c.getType(), c.getDetail()))); contacts.stream() .filter(c -> !user.getContacts().contains(c)) .forEach(c -> repository.addEvent( userId, new UserContactAddedEvent(c.getType(), c.getDetail()))); user.getAddresses().stream() .filter(a -> !addresses.contains(a)) .forEach(a -> repository.addEvent( userId, new UserAddressRemovedEvent(a.getCity(), a.getState(), a.getPostcode()))); addresses.stream() .filter(a -> !user.getAddresses().contains(a)) .forEach(a -> repository.addEvent( userId, new UserAddressAddedEvent(a.getCity(), a.getState(), a.getPostcode()))); } public Set getContactByType(String userId, String contactType) { User user = UserUtility.recreateUserState(repository, userId); return user.getContacts().stream() .filter(c -> c.getType().equals(contactType)) .collect(Collectors.toSet()); } public Set
getAddressByRegion(String userId, String state) throws Exception { User user = UserUtility.recreateUserState(repository, userId); return user.getAddresses().stream() .filter(a -> a.getState().equals(state)) .collect(Collectors.toSet()); } }

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

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

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

5.3. Преимущества и недостатки поиска событий

Теперь мы успешно внедрили альтернативный способ хранения объектов домена с использованием источника событий. Источник событий является мощным шаблоном и при правильном использовании приносит много преимуществ архитектуре приложения:

  • Делает операции записи намного быстрее так как нет необходимости в чтении, обновлении и записи; запись-это просто добавление события в журнал
  • Удаляет объектно-реляционный импеданс и, следовательно, необходимость в сложных инструментах отображения; конечно, нам все еще нужно воссоздать объекты обратно
  • Случается, что предоставляет журнал аудита в качестве побочного продукта , который полностью надежен; мы можем точно отладить, как изменилось состояние модели домена
  • Это позволяет поддерживать временные запросы и достигать перемещения во времени (состояние домена в какой-то момент в прошлом)!
  • Это естественно подходит для проектирования слабо связанных компонентов в архитектуре микросервисов, которые взаимодействуют асинхронно, обмениваясь сообщениями

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

  • Существует связанная кривая обучения и сдвиг в мышлении, необходимый для принятия источника событий; для начала это не интуитивно понятно
  • Это делает его довольно трудным для обработки типичных запросов , так как нам нужно воссоздать состояние, если мы не сохраняем его в локальном кэше
  • Хотя он может быть применен к любой модели домена, он более подходит для модели, основанной на событиях в архитектуре, управляемой событиями

6. CQRS с поиском событий

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

Давайте сначала посмотрим, как архитектура приложения объединяет их:

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

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

Здесь важно убедиться, что мы используем их для управления сложностью, а не просто для дальнейшего увеличения сложности!

6.1. Объединение CQR и источников событий

Внедрив поиск событий и CQR по отдельности, не должно быть так сложно понять, как мы можем объединить их.

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

Есть всего несколько изменений. Мы начнем с изменения агрегата на генерировать события вместо обновления состояния :

public class UserAggregate {
    private EventStore writeRepository;
    public UserAggregate(EventStore repository) {
        this.writeRepository = repository;
    }

    public List handleCreateUserCommand(CreateUserCommand command) {
        UserCreatedEvent event = new UserCreatedEvent(command.getUserId(), 
          command.getFirstName(), command.getLastName());
        writeRepository.addEvent(command.getUserId(), event);
        return Arrays.asList(event);
    }

    public List handleUpdateUserCommand(UpdateUserCommand command) {
        User user = UserUtility.recreateUserState(writeRepository, command.getUserId());
        List events = new ArrayList<>();

        List contactsToRemove = user.getContacts().stream()
          .filter(c -> !command.getContacts().contains(c))
          .collect(Collectors.toList());
        for (Contact contact : contactsToRemove) {
            UserContactRemovedEvent contactRemovedEvent = new UserContactRemovedEvent(contact.getType(), 
              contact.getDetail());
            events.add(contactRemovedEvent);
            writeRepository.addEvent(command.getUserId(), contactRemovedEvent);
        }
        List contactsToAdd = command.getContacts().stream()
          .filter(c -> !user.getContacts().contains(c))
          .collect(Collectors.toList());
        for (Contact contact : contactsToAdd) {
            UserContactAddedEvent contactAddedEvent = new UserContactAddedEvent(contact.getType(), 
              contact.getDetail());
            events.add(contactAddedEvent);
            writeRepository.addEvent(command.getUserId(), contactAddedEvent);
        }

        // similarly process addressesToRemove
        // similarly process addressesToAdd

        return events;
    }
}

Единственное другое изменение, которое требуется, – это проектор, который теперь должен обрабатывать события вместо состояний объектов домена :

public class UserProjector {
    UserReadRepository readRepository = new UserReadRepository();
    public UserProjector(UserReadRepository readRepository) {
        this.readRepository = readRepository;
    }

    public void project(String userId, List events) {
        for (Event event : events) {
            if (event instanceof UserAddressAddedEvent)
                apply(userId, (UserAddressAddedEvent) event);
            if (event instanceof UserAddressRemovedEvent)
                apply(userId, (UserAddressRemovedEvent) event);
            if (event instanceof UserContactAddedEvent)
                apply(userId, (UserContactAddedEvent) event);
            if (event instanceof UserContactRemovedEvent)
                apply(userId, (UserContactRemovedEvent) event);
        }
    }

    public void apply(String userId, UserAddressAddedEvent event) {
        Address address = new Address(
          event.getCity(), event.getState(), event.getPostCode());
        UserAddress userAddress = Optional.ofNullable(
          readRepository.getUserAddress(userId))
            .orElse(new UserAddress());
        Set
addresses = Optional.ofNullable(userAddress.getAddressByRegion() .get(address.getState())) .orElse(new HashSet<>()); addresses.add(address); userAddress.getAddressByRegion() .put(address.getState(), addresses); readRepository.addUserAddress(userId, userAddress); } public void apply(String userId, UserAddressRemovedEvent event) { Address address = new Address( event.getCity(), event.getState(), event.getPostCode()); UserAddress userAddress = readRepository.getUserAddress(userId); if (userAddress != null) { Set
addresses = userAddress.getAddressByRegion() .get(address.getState()); if (addresses != null) addresses.remove(address); readRepository.addUserAddress(userId, userAddress); } } public void apply(String userId, UserContactAddedEvent event) { // Similarly handle UserContactAddedEvent event } public void apply(String userId, UserContactRemovedEvent event) { // Similarly handle UserContactRemovedEvent event } }

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

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

Это в значительной степени все, что нам нужно сделать, чтобы объединить источники событий и CQR в нашем простом приложении.

7. Заключение

В этом уроке мы обсудили основы поиска событий и шаблоны проектирования CQRS. Мы разработали простое приложение и применили к нему эти шаблоны индивидуально.

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

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

Как обычно, исходный код этой статьи можно найти на GitHub .