Upgrade to Pro — share decks privately, control downloads, hide ads and more …

CQRS и Event Sourcing. Введение

CQRS и Event Sourcing. Введение

CQRS, Event Sourcing, DDD

Avatar for zloyuser

zloyuser

March 31, 2017
Tweet

More Decks by zloyuser

Other Decks in Programming

Transcript

  1. Принцип программирования, утверждающий, что каждая функция должна быть или командой,

    выполняющей действие, либо запросом, возвращающим данные, но не одновременно. “Объектно-ориентированное конструирование программных систем” Бертран Мейер, 1997 г. CQS
  2. CRUD interface CustomerService { public function getWithID(int $id): Customer; public

    function findByEmail(string $email): ?Customer; public function registerCustomer(string $email): int; public function changeEmail(int $id, string $newEmail): bool; public function makePreferred(int $id, float $discount): float; public function removeCustomer(int $id): bool; } CQS interface CustomerService { public function getWithID(int $id): Customer; public function findByEmail(string $email): ?Customer; public function registerCustomer(int $id, string $email): void; public function changeEmail(int $id, string $newEmail): void; public function makePreferred(int $id, float $discount): void; public function removeCustomer(int $id): void; } CRUD vs CQS
  3. Не паттерн. Не архитектура. Архитектурный паттерн (?) применяющий CQS на

    уровне приложения. “CQRS - это создание двух объектов, там где раньше был только один.” Грег Янг, 2010 г. CQRS
  4. CSQ vs CQRS CQS interface CustomerService { public function getWithID(int

    $id): Customer; public function findByEmail(string $email): ?Customer; public function registerCustomer(int $id, string $email): void; public function changeEmail(int $id, string $newEmail): void; public function makePreferred(int $id, float $discount): void; public function removeCustomer(int $id): void; } CQRS interface CustomerReadService { public function getWithID(int $id): Customer; public function findByEmail(string $email): ?Customer; } interface CustomerWriteService { public function registerCustomer(int $id, string $email): void; public function changeEmail(int $id, string $newEmail): void; public function makePreferred(int $id, float $discount): void; public function removeCustomer(int $id): void; }
  5. CRUD vs CQRS DTO Client CRUD Application DTO Domain Command

    Client Query Domain Write Application Read Application Database Database
  6. Архитектура построения программной системы, которая может принимать и отправлять сообщения

    с использованием одного или нескольких каналов связи, так чтобы приложения могли общаться без необходимости знать конкретные детали друг о друге. “Шаблоны интеграции корпоративных приложений” Грегор Хоп, Бобби Вульф, 2016 г. Message-Based Architecture
  7. Command • Изменяют состояние приложения • Могут быть асинхронными •

    Отображают действие пользователя • Имеют смысл в едином языке • Используют один обработчик • Именование в повелительном наклонении Типы сообщений в CQRS Query • Не меняют состояние приложения • Синхронная операция • Предоставляют данные клиенту • Используют один обработчик
  8. Command class RegisterCustomer { /** * @var string */ public

    $id; /** * @var string */ public $email; /** * @var string */ public $password; } Сообщения. Примеры Query class ListPreferredCustomers { /** * @var float */ public $minDiscount = 0.0; /** * @var int */ public $limit = 10; }
  9. CQRS vs CQRS + Сообщения CQRS interface CustomerReadService { public

    function getWithID(int $id): Customer; public function findByEmail(string $email): ?Customer; } interface CustomerWriteService { public function registerCustomer(int $id, string $email): void; public function changeEmail(int $id, string $newEmail): void; public function makePreferred(int $id, float $discount): void; public function removeCustomer(int $id): void; } CQRS + Сообщения interface CustomerReadService { public function getWithID(GetCustomerWithID $query): Customer; public function findByEmail(FindCustomerByEmail $query): ?Customer; } interface CustomerWriteService { public function registerCustomer(RegisterCustomer $command): void; public function changeEmail(ChangeCustomerEmail $command): void; public function makePreferred(MakeCustomerPreferred $command): void; public function removeCustomer(RemoveCustomer $command): void; }
  10. Две модели. Запись. class CustomerWriteService { public function registerCustomer(RegisterCustomer $command):

    void { $customer = Customer::register($command->id, $command->email); $algorithm = new Password\BCrypt(); $customer->changePassword($command->password, $algorithm); $this->repository->add($customer); } public function makePreferred(MakePreferred $command): void { $discount = new Discount($command->discount); $customer = $this->repository->find(new CustomerID($command->id)); if (null === $customer) { throw new Exception\CustomerNotFound($command->id); } $customer->makePreferred($discount); } }
  11. Две модели. Чтение. class CustomerReadService { public function getWithID(GetCustomerByID $query):

    CustomerDTO { $row = $this->db->table('customers')->find($query->id); if (null === $row) { throw new Exception\CustomerNotFound($query->id); } return new CustomerDTO($row->id, $row->email, $row->preferred); } public function findByEmail(FindCustomerByEmail $query): ?CustomerDTO { $row = $this->db->table('customers')->where('email', $query->email); return $row ? new CustomerDTO($row->id, $row->email, $row->preferred) : null; } }
  12. Еще один шаг Command Domain Write Application Client Query Read

    Application Write Database Как обновить данные? Read Database
  13. Паттерн программирования служащий для информирования приложения об изменениях в моделях

    предметной области. “Реализация методов предметно-ориентированного проектирования” Вон Вернон, 2016 г. Domain Event
  14. • Имеют смысл в едином языке • Неизменны • Могут

    обрабатываться асинхронно • Используют 0..N обработчиков • Именование в прошедшем времени Domain Event как сообщение
  15. Domain Event. Примеры class CustomerRegistered extends DomainEvent { /** *

    @var string */ public $id; /** * @var string */ public $email; /** * @var \DateTime */ public $date; } class CustomerBecamePreferred extends DomainEvent { /** * @var string */ public $id; /** * @var float */ public $discount; /** * @var float */ public $previousDiscount; }
  16. Domain Event. Aggregate Root class Customer extends AggregateRoot { /**

    * @var Discount */ private $discount; /** * @param Discount $discount */ public function makePreferred(Discount $discount): void { $event = new CustomerBecamePreferred(); $event->id = $this->id; $event->previousDiscount = $this->discount->getValue(); $event->discount = $discount->getValue(); $this->raise($event); $this->discount = $discount; } } abstract class AggregateRoot implements Entity { private $events = []; /** * @param DomainEvent $event */ public function raise(DomainEvent $event): void { $this->events[] = $event; } /** * @return DomainEvent[] */ public function pullEvents(): array { $events = $this->events; $this->events = []; return $events; } }
  17. Domain Event. Read Application class CustomerProjection { public function whenCustomerRegistered(CustomerRegistered

    $event): void { $this->db->table('customers')->insert([ 'id' => $event->id, 'email' => $event->email, 'created_at' => $event->date->format(\DateTime::W3C), 'preferred' => false, 'discount' => 0.00 ]); } public function whenCustomerBecamePreferred(CustomerBecamePreferred $event): void { $this->db->table('customers')->update([ 'id' => $event->id ], [ 'preferred' => true, 'discount' => $event->discount ]); } }
  18. Две базы данных Command Domain Write Application Client Query Read

    Application Write Database Read Database Events Денормализованные данные
  19. Несколько баз данных Command Domain Write Application Client Query Read

    Application Write Database Read Database Events Read Database
  20. Тестирование. События class CustomerTestCase extends CommandTestCase { public function createService():

    CustomerWriteService { $repository = new InMemoryCustomerRepository(); return new CustomerWriteService($repository); } public function testRegister(): void { $id = Uuid::uuid4(); $this ->when(new RegisterCustomer($id, '[email protected]')) ->then(new CustomerRegistered($id, '[email protected]')) ; } }
  21. Тестирование. Исключения class CustomerTestCase extends CommandTestCase { public function createService():

    CustomerWriteService { $repository = new InMemoryCustomerRepository(); return new CustomerWriteService($repository); } public function testRegisterWithInvalidEmail(): void { $id = Uuid::uuid4(); $this ->when(new RegisterCustomer($id, 'greg.young.gmail.com')) ->throws(new Exception\InvalidCustomerEmail('greg.young.gmail.com')) ; } }
  22. • Как менялась цена товара в последние три месяца? •

    Какова средняя сумма пополнения баланса? • Сколько корзин не проходят оформления? • Какой E-mail был у пользователя N дней назад? И тут бизнес хочет знать...
  23. Это способ сохранить состояние приложения, сохранив историю событий, которые определили

    текущее состояние. “Exploring CQRS and Event Sourcing: A journey into high scalability, availability, and maintainability with Windows Azure” Dominic Betts, 2013 г. Event Sourcing
  24. Event Sourcing в CQRS Command Domain Write Application Client Query

    Read Application Write Database Read Database Events
  25. ORM vs Event Sourcing id email prefered discount blocked created_at

    1 [email protected] 0 0.0 0 2017-03-02 2 [email protected] 1 5.5 0 2017-03-30 3 [email protected] 0 0.0 1 2017-01-09 id aggregate event payload recorded_on 1 Customer CustomerRegistered {"id": 1, "email": "[email protected]"} 2017-01-09 1 Customer CustomerMadePrefered {"id": 1, "discount": "10.0"} 2017-01-11 1 Customer CustomerBlocked {"id": 1, "reason": "Cheater!"} 2017-01-12 ORM Event Sourcing
  26. Event Sourcing. Aggregate Root abstract class EventSourced extends AggregateRoot {

    public function raise(DomainEvent $event): void { parent::raise($event); $name = end(explode('\\', get_class($event))); $method = sprintf('apply%s', $name); if (method_exists($this, $method)) call_user_func([$this, $method], $event); } public static function rebuild(array $events): self { $self = new static(); array_walk($events, [$self, 'raise']); $self->pullEvents(); return $self; } } abstract class AggregateRoot implements Entity { private $events = []; /** * @param DomainEvent $event */ public function raise(DomainEvent $event): void { $this->events[] = $event; } /** * @return DomainEvent[] */ public function pullEvents(): array { $events = $this->events; $this->events = []; return $events; } }
  27. Event Sourcing. Aggregate Root class Customer extends EventSourced { /**

    * @param Discount $discount */ public function makePreferred(Discount $discount): void { $event = new CustomerBecamePreferred(); $event->id = $this->id; $event->previousDiscount = $this->discount->getValue(); $event->discount = $discount->getValue(); $this->raise($event); } /** * @param CustomerBecamePreferred $event */ public function applyCustomerBecamePreferred($event): void { $this->discount = new Discount($event->discount); } } class Customer extends AggregateRoot { /** * @var Discount */ private $discount; /** * @param Discount $discount */ public function makePreferred(Discount $discount): void { $event = new CustomerBecamePreferred(); $event->id = $this->id; $event->previousDiscount = $this->discount->getValue(); $event->discount = $discount->getValue(); $this->raise($event); $this->discount = $discount; } }
  28. Event Sourcing в CQRS CustomerRegistered Field Value id 1 email

    [email protected] preferred 0 discount 0.0 blocked 0 CustomerBecamePreferred CustomerEmailChanged CustomerBlocked Customer Events Customer Aggregate
  29. Event Sourcing в CQRS CustomerRegistered Field Value id 1 email

    [email protected] preferred 1 discount 5.0 blocked 0 CustomerBecamePreferred CustomerEmailChanged CustomerBlocked Customer Events Customer Aggregate
  30. Event Sourcing в CQRS CustomerRegistered Field Value id 1 email

    [email protected] preferred 1 discount 5.0 blocked 0 CustomerBecamePreferred CustomerEmailChanged CustomerBlocked Customer Events Customer Aggregate
  31. Event Sourcing в CQRS CustomerRegistered Field Value id 1 email

    [email protected] preferred 1 discount 5.0 blocked 1 CustomerBecamePreferred CustomerEmailChanged CustomerBlocked Customer Events Customer Aggregate
  32. DDD + CQRS + ES • Структурирование кода • Уменьшение

    сложности • Производительность • Масштабирование • Тестирование • Бизнес требования
  33. • Версионирование событий • Process Manager и Saga • Очереди

    сообщений • ACID, BASE и прочие транзакции • CAP теорема и Eventual consistency • Снапшоты в Event Sourcing За кадром
  34. CQRS не архитектура? CQRS не архитектура верхнего уровня. Ваше приложение

    будет построено с применением SOA, MBA, DDD, etc. и лишь часть его будет использовать CQRS и, возможно, Event Sourcing. SOA - Service-Oriented Architecture MBA - Message-Based Architecture DDD - Domain Driven Design
  35. • Простые проекты с минимум логики: ToDo, блоги, сайты. •

    CRUD ориентированные приложения. Когда не стоит применять CQRS?