5. Моделювання фактора часу. Подієво-орієнтована архітектура.
5. Моделювання фактора часу. Подієво-орієнтована архітектура.
У попередньому розділі розглядався патерн моделі предметної області: його будівельні блоки, призначення та прикладний контекст.
Більше того, у ньому використовуються ті ж тактичні патерни, що і в моделі предметної області: об'єкти-значення (value object), агрегати (aggregate) і події предметної області (domain events).
Різниця між цими патернами реалізації полягає у способі збереження стану агрегатів. У моделі предметної області, заснованій на подіях, для управління станами агрегатів використовується патерн «Події як джерело даних» (Event Sourcing): замість збереження стану агрегату модель генерує події предметної області, які описують кожну зміну, і використовує їх як довірене джерело даних для агрегату.
Цей розділ починається з введення поняття «Події як джерело даних» (Event Sourcing). Потім ми розглянемо, як ця модель поєднується з патерном моделі предметної області, що перетворює її на модель предметної області, засновану на подіях (event-sourced domain model).
Події як джерело даних (Event Sourcing)
«Покажіть мені вашу блок-схему і сховайте ваші таблиці — я нічого не зрозумію. Але покажіть мені ваші таблиці — і мені навряд чи знадобиться ваша блок-схема, все стане зрозумілим, як на долоні». — Фред Брукс (Fred Brooks)
Щоб дати визначення патерну «Події як джерело даних» (Event Sourcing) і зрозуміти, чим він відрізняється від традиційного моделювання та збереження даних, звернімося до міркувань Фреда Брукса. Розгляньте табл. 7.1 і спробуйте зрозуміти, до яких висновків про систему, до якої вона належить, можна прийти на її основі.
Таблиця 7.1. Поточний стан лідів
| lead-id | first-name | last-name | status | phone-number | created-on | updated-on |
|---|---|---|---|---|---|---|
| 1 | Sean | Callahan | CONVERTED | 555-1246 | 2019-01-31T10:02:40.32Z | 2019-01-31T10:02:40.32Z |
| 2 | Sarah | Estrada | CLOSED | 555-4395 | 2019-03-29T22:01:41.44Z | 2019-03-29T22:01:41.44Z |
| 3 | Stephanie | Brown | CLOSED | 555-1176 | 2019-04-15T23:08:45.59Z | 2019-04-15T23:08:45.59Z |
| 4 | Sami | Calhoun | CLOSED | 555-1850 | 2019-04-25T05:42:17.07Z | 2019-04-25T05:42:17.07Z |
| 5 | William | Smith | CONVERTED | 555-3013 | 2019-05-14T04:43:57.51Z | 2019-05-14T04:43:57.51Z |
| 6 | Sabri | Chan | NEW_LEAD | 555-2900 | 2019-06-19T15:01:49.68Z | 2019-06-19T15:01:49.68Z |
| 7 | Samantha | Espinosa | NEW_LEAD | 555-8861 | 2019-07-17T13:09:59.32Z | 2019-07-17T13:09:59.32Z |
| 8 | Hani | Cronin | CLOSED | 555-3018 | 2019-10-09T11:40:17.13Z | 2019-10-09T11:40:17.13Z |
| 9 | Sian | Espinoza | FOLLOWUP_SET | 555-6461 | 2019-12-04T01:49:08.05Z | 2019-12-04T01:49:08.05Z |
| 10 | Sophia | Escamilla | CLOSED | 555-4090 | 2019-12-06T09:12:32.56Z | 2019-12-06T09:12:32.56Z |
| 11 | William | White | FOLLOWUP_SET | 555-1187 | 2020-01-23T00:33:13.88Z | 2020-01-23T00:33:13.88Z |
| 12 | Casey | Davis | CONVERTED | 555-8101 | 2020-05-20T09:52:55.95Z | 2020-05-27T12:38:44.12Z |
| 13 | Walter | Connor | NEW_LEAD | 555-4753 | 2020-04-20T06:52:55.95Z | 2020-04-20T06:52:55.95Z |
| 14 | Sophie | Garcia | CONVERTED | 555-1284 | 2020-05-06T18:47:04.70Z | 2020-05-06T18:47:04.70Z |
| 15 | Sally | Evans | PAYMENT_FAILED | 555-3230 | 2020-06-04T14:51:06.15Z | 2020-06-04T14:51:06.15Z |
| 16 | Scott | Chatman | NEW_LEAD | 555-6953 | 2020-06-09T09:07:05.23Z | 2020-06-09T09:07:05.23Z |
| 17 | Stephen | Pinkman | CONVERTED | 555-2326 | 2020-07-20T00:56:59.94Z | 2020-07-20T00:56:59.94Z |
| 18 | Sara | Elliott | PENDING_PAYMENT | 555-2620 | 2020-08-12T17:39:43.25Z | 2020-08-12T17:39:43.25Z |
| 19 | Sadie | Edwards | FOLLOWUP_SET | 555-8163 | 2020-10-22T12:40:03.98Z | 2020-10-22T12:40:03.98Z |
| 20 | William | Smith | PENDING_PAYMENT | 555-9273 | 2020-11-13T08:14:07.17Z | 2020-11-13T08:14:07.17Z |
Очевидно, що таблиця використовується для управління потенційними клієнтами або лідами в системі телемаркетингу. Для кожного ліда можна побачити його ідентифікатор, ім'я та прізвище, дата створення й оновлення запису, номер телефону та поточний статус.
Розглядаючи різні статуси, можна припустити, через які етапи обробки проходить кожен потенційний клієнт:
- Продажі починаються з потенційного клієнта у статусі
NEW_LEAD. Дзвінок може завершитися тим, що людина не зацікавлена у пропозиції (лідCLOSED), погоджується на повторний дзвінок (FOLLOWUP_SET) або приймає пропозицію продажу (PENDING_PAYMENT). - Якщо платіж пройшов успішно, лід перетворюється на клієнта (
CONVERTED). В іншому випадку платіж може бути неуспішним (PAYMENT_FAILED).
Це значний обсяг інформації, яку можна отримати шляхом простого аналізу схеми таблиці та даних, що в ній зберігаються. Також можна припустити, яку єдину мову (ubiquitous language) було використано під час моделювання даних.
Чого бракує в таблиці?
Ми не можемо проаналізувати, що відбувалося протягом життєвого циклу лідів. Невідомо, скільки дзвінків було зроблено до того, як лід став CONVERTED. Чи покупка була здійснена одразу, чи це був тривалий шлях продажу? Чи варто, спираючись на історичні дані, спробувати зв'язатися з людиною після кількох повторних звернень, або ж краще закрити лід і перейти до перспективнішого потенційного клієнта?
Усе, що ми знаємо, — це поточний стан потенційних клієнтів.
Поставлені питання відображають інтереси бізнесу, необхідні для оптимізації процесу продажу. З точки зору бізнесу важливо аналізувати дані й оптимізувати процес на основі отриманого досвіду. Один зі способів заповнення браку інформації — це використання подій як джерела для поточного стану (Event Sourcing).
Патерн «Події як джерело даних» (Event Sourcing)
Замість схеми, яка відображає поточний стан агрегатів, система, де джерелом поточного стану є події, зберігає події, що фіксують кожну зміну в життєвому циклі агрегата.
Розглянемо CONVERTED-клієнта з рядка 12 таблиці 7.1. У наступному листингу показано, як дані про цю людину будуть представлені в системі, заснованій на подіях:
[
{
"lead-id": 12,
"event-id": 0,
"event-type": "lead-initialized",
"first-name": "Casey",
"last-name": "David",
"phone-number": "555-2951",
"timestamp": "2020-05-20T09:52:55.95Z"
},
{
"lead-id": 12,
"event-id": 1,
"event-type": "contacted",
"timestamp": "2020-05-20T12:32:08.24Z"
},
{
"lead-id": 12,
"event-id": 2,
"event-type": "followup-set",
"followup-on": "2020-05-27T12:00:00.00Z",
"timestamp": "2020-05-20T12:32:08.24Z"
},
{
"lead-id": 12,
"event-id": 3,
"event-type": "contact-details-updated",
"first-name": "Casey",
"last-name": "Davis",
"phone-number": "555-8101",
"timestamp": "2020-05-20T12:32:08.24Z"
},
{
"lead-id": 12,
"event-id": 4,
"event-type": "contacted",
"timestamp": "2020-05-27T12:02:12.51Z"
},
{
"lead-id": 12,
"event-id": 5,
"event-type": "order-submitted",
"payment-deadline": "2020-05-30T12:02:12.51Z",
"timestamp": "2020-05-27T12:02:12.51Z"
},
{
"lead-id": 12,
"event-id": 6,
"event-type": "payment-confirmed",
"status": "converted",
"timestamp": "2020-05-27T12:38:44.12Z"
}
]
Сценарії, пов'язані з клієнтом
Події, відображені в листингу, розповідають про історію клієнта. Лід був створений в системі (подія 0), і через приблизно дві години з ним зв'язався торговий агент (подія 1). Під час дзвінка було домовлено, що продавець передзвонить через тиждень (подія 2), але на інший номер телефону (подія 3). Також продавець виправив помилку в прізвищі (подія 3). У домовлений день і час з лідом зв'язалися (подія 4) і відправили замовлення (подія 5). Замовлення мало бути оплачено через три дні (подія 5), але оплата надійшла через півгодини (подія 6), і лід був конвертований в нового покупця.
Як вже було сказано раніше, стан клієнта можна легко спроєктувати за допомогою цих подій предметної області. Потрібно лише по черзі застосувати просту логіку перетворення до кожної події:
public class LeadSearchModelProjection
{
public long Leadid { get; private set; }
public HashSet<string> FirstNames { get; private set; }
public HashSet<string> LastNames { get; private set; }
public HashSet<PhoneNumber> PhoneNumbers { get; private set; }
public int Version { get; private set; }
public void Apply(LeadInitialized @event)
{
Leadid = @event.Leadid;
FirstNames = new HashSet<string>{};
LastNames = new HashSet<string>{};
PhoneNumbers = new HashSet<PhoneNumber>();
FirstNames.Add(@event.FirstName);
LastNames.Add(@event.LastName);
PhoneNumbers.Add(@event.PhoneNumber);
Version = 0;
}
public void Apply(ContactDetailsChanged @event)
{
FirstNames.Add(@event.FirstName);
LastNames.Add(@event.LastName);
PhoneNumbers.Add(@event.PhoneNumber);
Version += 1;
}
public void Apply(Contacted @event)
{
Version += 1;
}
public void Apply(FollowupSet @event)
{
Version += 1;
}
public void Apply(OrderSubmitted @event)
{
Version += 1;
}
public void Apply(PaymentConfirmed @event)
{
Version += 1;
}
}
Ітерація по подіях агрегату та їх поетапне передавання в відповідний перевизначений метод Apply створить таке ж представлення стану, яке змодельоване в таблиці 7.1.
Version, значення якого збільшується після застосування кожної події. Його значення представляє собою загальну кількість модифікацій, внесених в бізнес-сутність.Більше того, припустимо, що ми застосували до агрегату лише певну підмножину подій. У такому випадку виникає можливість "подорожувати в часі": звертаючись лише до відповідних подій, можна спроєктувати стан сутності в будь-який момент її життєвого циклу. Наприклад, якщо потрібно отримати стан об'єкта на версії 5, можна звернутися лише до перших п'яти подій.
І нарешті, ми не обмежені лише однією проекцією стану! Розглянемо наступні сценарії.
Пошук
Уявімо, що виникла необхідність у реалізації пошуку. Але оскільки контактна інформація потенційного клієнта (ліда) — ім'я, прізвище та номер телефону — може бути оновлена, агенти з продажу можуть не знати про зміни, внесені іншими агентами, і можуть захотіти знайти потенційних клієнтів, використовуючи свою контактну інформацію, включаючи раніше зафіксовані значення. Хронологічну інформацію можна легко спроєктувати:
public class LeadSearchModelProjection
{
public long Leadid { get; private set; }
public HashSet<string> FirstNames { get; private set; }
public HashSet<string> LastNames { get; private set; }
public HashSet<PhoneNumber> PhoneNumbers { get; private set; }
public int Version { get; private set; }
public void Apply(LeadInitialized @event)
{
Leadid = @event.Leadid;
FirstNames = new HashSet<string>();
LastNames = new HashSet<string>();
PhoneNumbers = new HashSet<PhoneNumber>();
FirstNames.Add(@event.FirstName);
LastNames.Add(@event.LastName);
PhoneNumbers.Add(@event.PhoneNumber);
Version = 0;
}
public void Apply(ContactDetailsChanged @event)
{
FirstNames.Add(@event.FirstName);
LastNames.Add(@event.LastName);
PhoneNumbers.Add(@event.PhoneNumber);
Version += 1;
}
public void Apply(Contacted @event)
{
Version += 1;
}
public void Apply(FollowupSet @event)
{
Version += 1;
}
public void Apply(OrderSubmitted @event)
{
Version += 1;
}
public void Apply(PaymentConfirmed @event)
{
Version += 1;
}
}
Для заповнення відповідних наборів особистих даних ліда в логіці проекції використовуються події LeadInitialized та ContactDetailsChanged. Інші події ігноруються, оскільки вони не впливають на стан конкретної моделі.
Застосування цієї логіки проекції до подій Кейсі Девіса (Casey Davis) з попереднього прикладу призведе до наступного стану:
LeadId: 12
FirstNames: ['Casey']
LastNames: ['David', 'Davis']
PhoneNumbers: ['555-2951', '555-8101']
Version: 6
Аналіз
Відділ бізнес-аналітики вашої компанії просить надати зручніше для аналізу представлення даних про потенційних клієнтів. Для свого поточного дослідження вони хочуть отримати кількість подальших дзвінків, запланованих для різних лідів. Згодом вони будуть фільтрувати дані про конверсії та закриті ліди і використовувати модель для оптимізації процесу продажу. Давайте спроєктуємо запитувані ними дані:
public class AnalysisModelProjection
{
public long Leadid { get; private set; }
public int Followups { get; private set; }
public LeadStatus Status { get; private set; }
public int Version { get; private set; }
public void Apply(LeadInitialized @event)
{
Leadid = @event.Leadid;
Followups = 0;
Status = LeadStatus.NEW_LEAD;
Version = 0;
}
public void Apply(Contacted @event)
{
Version += 1;
}
public void Apply(FollowupSet @event)
{
Status = LeadStatus.FOLLOWUP_SET;
Followups += 1;
Version += 1;
}
public void Apply(ContactDetailsChanged @event)
{
Version += 1;
}
public void Apply(OrderSubmitted @event)
{
Status = LeadStatus.PENDING_PAYMENT;
Version += 1;
}
public void Apply(PaymentConfirmed @event)
{
Status = LeadStatus.CONVERTED;
Version += 1;
}
}
Показана вище логіка підтримує лічильник кількості появ наступних дзвінків у подіях ліда. Якщо застосувати цю проекцію, наприклад, до подій агрегату, вона згенерує наступний стан:
LeadId: 12
Followups: 1
Status: Converted
Version: 6
Логіка, реалізована в попередніх прикладах, проєктує моделі, оптимізовані для пошуку та аналізу, в пам'ять. Але для фактичної реалізації вимоганої функціональності спроєктовані моделі потрібно зберігати в базі даних.
Джерело істини
Цей процес показаний на рис. 7.1.

Рис. 7.1. Агрегат, заснований на подіях.
База даних, в якій зберігаються системні події, — це єдине строго узгоджене сховище: системне джерело істини. Базу даних, яка використовується для збереження подій, прийнято називати сховищем подій (event store).
Сховище подій
Сховище подій (event store) не повинно дозволяти змінювати або видаляти події, оскільки воно призначене тільки для їх збереження. Для підтримки реалізації патерна «Події як джерело даних» сховище подій повинно підтримувати як мінімум наступні операції:
- Витягування всіх подій, що належать певній бізнес-сутності.
- Додавання подій.
Наприклад:
interface IEventStore
{
IEnumerable<Event> Fetch(Guid instanceId);
void Append(Guid instanceId, Event[] newEvents, int expectedVersion);
}
Аргумент expectedVersion методу Append необхідний для реалізації оптимістичного управління конкурентним доступом: при додаванні нових подій також вказується версія сутності, на якій ґрунтуються ваші рішення.
Практична реалізація
У більшості систем для реалізації патерна CQRS потрібні додаткові ендпоінти, про які йтиметься в наступному розділі.
Події як джерело даних у фінансовій індустрії
Реєстр — це журнал, призначений лише для додавання записів, в якому документуються транзакції. Поточний стан (наприклад, баланс рахунку) завжди можна отримати шляхом «проєкції» записів реєстру.
Модель предметної області, основана на подіях
Існуюча модель предметної області працює через відображення стану своїх агрегатів і породжує події предметної області. Модель предметної області, основана на подіях, використовує події предметної області виключно для моделювання життєвих циклів агрегатів. Всі зміни стану агрегату повинні бути виражені у вигляді подій предметної області.
Сценарій операцій з агрегатом, заснованим на подіях
- Завантаження подій предметної області агрегату.
- Відновлення стану — проєкція подій у представлення стану, яке може бути використано для прийняття бізнес-рішень.
- Виконання команди бізнес-логіки агрегату і, відповідно, породження нових подій предметної області.
- Фіксація нових подій предметної області у сховищі подій (event store).
Приклад реалізації агрегату Ticket на основі подій
Розглянемо код прикладу з агрегатом Ticket з глави 6 і його реалізацію як агрегат на основі подій.
Приклад коду, що слідує зазначеному сценарію:
public class TicketAPI
{
private ITicketsRepository _ticketsRepository;
public void RequestEscalation(Ticketid id, EscalationReason reason)
{
var events = ticketsRepository.LoadEvents(id);
var ticket = new Ticket(events);
var originalVersion = ticket.Version;
var cmd = new RequestEscalation(reason);
ticket.Execute(cmd);
_ticketsRepository.CommitChanges(ticket, originalVersion);
}
}
Відповідно до логіки відновлення агрегату Ticket у конструкторі (рядки з 27-го по 31-й), створюється екземпляр класу проєкції стану TicketState, і для кожної події заявки послідовно викликається її метод AppendEvent:
public class Ticket
{
private List<DomainEvent> _domainEvents = new List<DomainEvent>();
private TicketState _state;
public Ticket(IEnumerable<IDomainEvent> events)
{
state = new TicketState();
foreach (var e in events)
{
AppendEvent(e);
}
}
private void AppendEvent(IDomainEvent @event)
{
_domainEvents.Append(@event);
// Визов необхідного методу "Apply" в динамічному режимі
((dynamic)state).Apply((dynamic)@event);
}
}
У відмінність від реалізації, показаної в попередній главі, метод RequestEscalation, що належить агрегату, чиє стан відновлюється на основі подій, явно не встановлює прапор IsEscalated в значення true. Замість цього він породжує відповідну подію і передає її методу AppendEvent:
public void Execute(RequestEscalation cmd)
{
if (!_state.IsEscalated && _state.RemainingTimePercentage <= 0)
{
var escalatedEvent = new TicketEscalated(_id, cmd.Reason);
AppendEvent(escalatedEvent);
}
}
Таким чином, кожна операція з агрегатом в даній моделі базується на подіях, що є основою для відновлення стану агрегату та подальшого виконання бізнес-логіки.
Клас TicketState:
public class TicketState
{
public Ticketid Id { get; private set; }
public int Version { get; private set; }
public bool IsEscalated { get; private set; }
public void Apply(Ticketinitialized @event)
{
Id = @event.Id;
Version = 0;
IsEscalated = false;
}
public void Apply(TicketEscalated @event)
{
IsEscalated = true;
Version += 1;
}
}
Переваги використання патерну "Події як джерело даних" (Event Sourcing)
Порівняно з більш традиційною моделлю, в якій поточні стани агрегатів зберігаються в базі даних, модель предметної області, основана на подіях (event-sourced domain model), вимагає для моделювання агрегатів більше зусиль. Але цей підхід дає суттєві переваги:
- Подорож у часі: Події предметної області можна використовувати не лише для відновлення поточного стану агрегата, але й для відновлення всіх минулих станів агрегата. Це часто використовується при аналізі поведінки системи, перевірці рішень системи та оптимізації бізнес-логіки.
- Глибоке розуміння суті подій: Використання подій як джерела даних надає гнучку модель, яка дозволяє виконувати перетворення подій у різні уявлення стану. У будь-який момент можна додавати аналітичні оцінки, базуючись на минулих подіях.
- Журнал аудиту: Збережені події предметної області є строго узгодженим журналом аудиту всього, що відбулося з агрегатами. Закони вимагають введення таких журналів аудиту в деяких сферах бізнесу.
- Розширене оптимістичне управління паралельними обчисленнями: Класична модель оптимістичного паралелізму видає виключення, коли вже зчитані дані застарівають. Використовуючи патерн "Події як джерело даних", можна отримати глибше розуміння того, що відбулося між зчитуванням подій та записом нових.
Недоліки
::caution 5. Крива навчання: Очевидним недоліком патерну є його різка відмінність від традиційних способів управління даними. Швидка реалізація вимагає освоєння командою та часу на адаптацію до нової логіки.
- Розвиток моделі: Розвиток моделі, заснованої на подіях, може бути складним. Згідно з чітким визначенням патерну "Події як джерело даних", події є незмінними (immutable). Як тоді змінити схему подій? Цей процес складніший за зміну схеми таблиць.
- Архітектурна складність: Реалізація патерну "Події як джерело даних" не обходиться без численних архітектурних "рухомих частин", що ускладнює загальну конструкцію.
::
Ці проблеми стають ще більш суттєвими, якщо використання патерну не виправдовує поставлене завдання, яке можна вирішити за допомогою простішого дизайну.
Продуктивність
Відновлення стану агрегату з подій негативно впливає на продуктивність системи, і з часом, із додаванням нових подій, ситуація лише погіршуватиметься. Як це взагалі може працювати?
Проекція подій у представлення стану дійсно потребує обчислювальних ресурсів, і з кожною новою подією потреба в них буде збільшуватись. Важливо оцінити вплив проекції на продуктивність: наслідки роботи з сотнями або тисячами подій.
У рідкісних випадках, коли проекція станів стає проблемою продуктивності, можна реалізувати інший патерн: зріз стану (snapshot).
У цьому патерні, показаному на рис. 7.2, виконуються наступні кроки:
- Процес виконує безперервний перебір нових подій у сховищі, генерує відповідні проекції та зберігає їх у кеші.
- Проекція в пам'яті (in-memory) необхідна для виконання дії над агрегатом.
При цьому:
- Процес витягує проекцію поточного стану з кешу.
- Процес витягує події, що відбулися після останньої версії зрізу стану зі сховища подій.
- До зрізу стану (snapshot) у пам'яті застосовуються додаткові події.
Чи можна масштабувати цю модель?
Модель, заснована на подіях, легко піддається масштабуванню. Оскільки всі операції, пов'язані з агрегатом, виконуються в контексті одного єдиного агрегату, сховище подій можна сегментувати за ідентифікаторами агрегату: всі події, що належать екземпляру агрегату, повинні знаходитися в одному сегменті.

Видалення даних
Хранилище подій — це база даних, призначена тільки для додавання, але що робити, якщо виникне потреба у фізичному видаленні даних, наприклад, для дотримання Загального регламенту із захисту даних (GDPR)?
Ключ шифрування зберігається у зовнішньому сховищі типу ключ-значення, тобто у сховищі ключів, де ключ — це ідентифікатор певного агрегата, а значення — ключ шифрування. Коли конфіденційні дані мають бути видалені, ключ шифрування видаляється зі сховища ключів. У результаті конфіденційна інформація, яка міститься в подіях, стає недоступною.
А чому просто не можна...
Запис даних одночасно до робочої бази даних і до файлу журналу є операцією, яка може завершитися помилкою. По суті, це транзакція, що охоплює два механізми зберігання: базу даних і файл. Якщо перший механізм не спрацював, із другим потрібно провести операцію відкату.
Отже, такі журнали не будуть узгодженими і, скоріше за все, з часом стануть суперечити один одному.
З погляду інфраструктури такий підхід забезпечує узгоджену синхронізацію стану і записів журналу. Але все ж імовірність виникнення помилок залишається. Що, якщо програміст, який супроводжуватиме код у майбутньому, просто забуде додати відповідний запис у журнал?
Такий підхід усуне недолік попереднього варіанта: явні ручні виклики для додавання записів до журнальної таблиці вже не знадобляться. Але при цьому отримана історія міститиме лише сухі факти: які саме поля були змінені. А бізнес-контекст — чому поля були змінені — залишиться невідомим.
Висновок
У цій глави розглянуто патерн «Події як джерело даних» (Event Sourcing) та питання його застосування для моделювання фактора часу в агрегатах моделі предметної області.
Цією главою завершується наше дослідження різних способів моделювання та реалізації бізнес-логіки. У наступній главі наша увага буде переключена на патерни вищого рівня, що безпосередньо стосуються архітектури системи.
Завдання для перевірки
- Які відносини між подіями предметної області та об'єктами-значеннями?
- А) Події використовують об'єкти-значення для опису змін.
- Б) У моделі подій об'єкти-значення замінюються на агрегати.
- В) Об'єкти-значення замінюються подіями предметної області.
- Г) Усі варіанти неправильні.
- Що справедливо щодо проєкцій стану з послідовності подій?
- А) З подій можна спроєктувати лише одне представлення стану.
- Б) Можна спроєктувати декілька представлень, але події мають підтримувати їх.
- В) Декілька проєкцій можливо додати пізніше.
- Г) Усі варіанти неправильні.
- Що відрізняє агрегати на основі подій від агрегатів на основі стану?
- А) Тільки агрегати на основі подій породжують події.
- Б) Події можуть бути джерелом істини лише для агрегатів на основі подій.
- В) Агрегати на основі подій гарантують зв’язок кожної зміни із подією.
- Г) Вірні варіанти «Б» та «В».
4. Опрацювання складної бізнес-логіки
https://github.com/arakviel/library-ddd-csharp
6. Архітектурні патерни
У попередніх тактичних патернах, розглянутих у цій книзі, описувалися різні способи моделювання та реалізації бізнес-логіки. У цій главі тактичний дизайн розглядатиметься в ширшому контексті, що включає різноманітні способи координації взаємодій і залежностей між компонентами системи.