فصل ۷ — مدلسازی بُعد زمان
در فصل قبل با الگوی Domain Model، بلوکهای سازنده، هدف و زمینهٔ کاربرد آن آشنا شدید. مدل دامنهٔ رویدادمنبع (Event-Sourced Domain Model) بر همان فرضها بنا شده است: منطق کسبوکار پیچیده است و به زیردامنهٔ هستهای تعلق دارد. این مدل از همان الگوهای تاکتیکی Domain Model، یعنی Value Objectها، Aggregateها و Domain Eventها استفاده میکند.
تفاوت اصلی در شیوهٔ Persist کردن State Aggregate است. Event-Sourced Domain Model برای مدیریت State از Event Sourcing استفاده میکند: بهجای ذخیرهٔ وضعیت فعلی Aggregate، رویدادهای دامنهای تولید میشوند که هر تغییر را توصیف میکنند و همین رویدادها به Source of Truth دادهٔ Aggregate تبدیل میشوند.
فصل با معرفی Event Sourcing آغاز میشود و سپس نشان میدهد چگونه میتوان آن را با Domain Model ترکیب کرد.
Event Sourcing
نمودار جریان خود را به من نشان بده و جدولهایت را پنهان کن؛ همچنان گیج خواهم ماند. جدولهایت را نشان بده و معمولاً دیگر به نمودار جریان نیاز ندارم؛ همهچیز روشن خواهد بود.
Fred Brooks
برای تعریف Event Sourcing و تفاوت آن با مدلسازی و ذخیرهسازی سنتی داده، جدول ۷-۱ را بررسی کنید و ببینید فقط از Schema و دادهٔ آن چه چیزی دربارهٔ سامانه میتوان فهمید.
جدول ۷-۱ — مدل مبتنی بر وضعیت
| lead-id | first-name | last-name | status | phone-number | followup-on | 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 | 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 | 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 | 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 |
آشکار است که جدول برای مدیریت مشتریان بالقوه یا Leadها در یک سامانهٔ بازاریابی تلفنی استفاده میشود. برای هر Lead شناسه، نام و نام خانوادگی، زمان ایجاد و Update، شماره تلفن و Status فعلی قابل مشاهده است.
با بررسی Statusها حتی میتوان چرخهٔ پردازش هر مشتری بالقوه را حدس زد:
- جریان فروش با NEW_LEAD آغاز میشود.
- تماس فروش ممکن است با عدم علاقه و CLOSED، تعیین تماس بعدی FOLLOWUP_SET یا پذیرش پیشنهاد و PENDING_PAYMENT پایان یابد.
- اگر پرداخت موفق باشد Lead به مشتری CONVERTED میشود؛ در غیر این صورت PAYMENT_FAILED.
این حجم زیادی از اطلاعات است که فقط از Schema و Data جدول به دست میآید و حتی میتوان زبان فراگیر مدلسازی را حدس زد. اما چه چیزی گم شده است؟
دادهٔ جدول فقط وضعیت فعلی Lead را ثبت میکند و داستان رسیدن به آن وضعیت را از دست میدهد. نمیدانیم پیش از CONVERTED شدن چند تماس گرفته شده، خرید فوراً رخ داده یا پس از فرایندی طولانی، و آیا پس از چند Follow-up هنوز تماس بیشتر ارزش دارد یا بهتر است Lead بسته شود. این پرسشها برای بهینهسازی فرایند فروش حیاتیاند، اما در مدل مبتنی بر State حاضر نیستند.
Event Sourcing بُعد زمان را وارد مدل داده میکند. بهجای نگهداری فقط وضعیت فعلی Aggregate، سامانه رویدادهایی را Persist میکند که تمام تغییرات چرخهٔ عمر را مستند میکنند.
Lead شمارهٔ ۱۲ را در نظر بگیرید. در سامانهٔ Event-Sourced دادهٔ او میتواند چنین باشد:
{
"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"
}
این رویدادها داستان مشتری را روایت میکنند: Lead ایجاد شد، حدود دو ساعت بعد تماس انجام شد، تماس بعدی برای یک هفته بعد تعیین شد، شماره تلفن و غلط نام خانوادگی اصلاح شد، در زمان توافقشده تماس دوباره انجام شد، سفارش ثبت شد و با وجود مهلت سهروزه، پرداخت حدود نیمساعت بعد انجام شد و Lead به مشتری تبدیل شد.
State مشتری را میتوان از این Domain Eventها با اعمال ترتیبی منطق Projection بازسازی کرد:
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; }
}
با عبور ترتیبی از رویدادهای Aggregate و فراخوانی overload مناسب Apply دقیقاً State مدلشده در جدول ۷-۱ تولید میشود.
فیلد Version پس از هر رویداد افزایش مییابد و تعداد کل تغییرات موجودیت را نشان میدهد. اگر فقط بخشی از رویدادها اعمال شوند، میتوان «در زمان سفر کرد» و State موجودیت را در هر نقطه از چرخهٔ عمر بازسازی کرد؛ مثلاً برای Version 5 فقط پنج رویداد نخست را اعمال کنیم.
همچنین مجبور نیستیم فقط یک نمایش State از رویدادها بسازیم.
جستوجو
فرض کنید Agentها میخواهند Lead را با اطلاعات تماس فعلی یا تاریخی پیدا کنند، چون نام یا شمارهٔ تلفن ممکن است توسط Agent دیگری تغییر کرده باشد. Projection زیر همهٔ مقادیر تاریخی را حفظ میکند:
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 مجموعههای اطلاعات شخصی را پر میکنند و رویدادهای دیگر چون بر مدل جستوجو اثر ندارند فقط Version را جلو میبرند. نتیجه برای Casey Davis:
LeadId: 12
FirstNames: ['Casey']
LastNames: ['David', 'Davis']
PhoneNumbers: ['555-2951', '555-8101']
Version: 6
تحلیل
فرض کنید واحد Business Intelligence میخواهد برای هر Lead تعداد Follow-upها را ببیند تا بعداً Leadهای Converted و Closed را تحلیل و فرایند فروش را بهینه کند. Projection تحلیلی:
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
نمونهها Projection را در حافظه میسازند، اما برای قابلیت واقعی باید مدلهای Projection در پایگاه داده Persist شوند. فصل ۸ الگوی CQRS را برای این هدف معرفی میکند.
Source of Truth
برای Event Sourcing، تمام تغییرات State یک شیء باید به شکل رویداد نمایش داده و Persist شوند. همین رویدادها Source of Truth سامانه میشوند؛ شکل ۷-۱.
شکل ۷-۱ — Aggregate مبتنی بر Event Sourcing.
پایگاه دادهٔ ذخیرهکنندهٔ رویدادها تنها Storage با Strong Consistency و منبع حقیقت سیستم است. نام رایج آن Event Store است.
Event Store
Event Store باید Append-only باشد و اجازهٔ تغییر یا حذف رویدادها را ندهد. حداقل باید دو قابلیت داشته باشد: Fetch همهٔ رویدادهای متعلق به یک Business Entity و Append کردن رویدادهای جدید:
interface IEventStore
{
IEnumerable<Event> Fetch(Guid instanceId);
void Append(Guid instanceId, Event[] newEvents, int expectedVersion);
}
پارامتر expectedVersion برای Optimistic Concurrency است: هنگام Append، Version مبنای تصمیم را نیز اعلام میکنید. اگر پس از آن Version رویداد جدیدی اضافه شده باشد، Event Store باید Concurrency Exception ایجاد کند.
Event-Sourced Domain Model
Domain Model کلاسیک نمایش State فعلی Aggregate را نگه میدارد و فقط برخی Domain Eventها را منتشر میکند. Event-Sourced Domain Model در مقابل، Domain Eventها را بهطور انحصاری برای مدلکردن چرخهٔ عمر Aggregate به کار میگیرد؛ تمام تغییرات State باید به Domain Event تبدیل شوند.
هر عملیات روی Event-Sourced Aggregate این مراحل را طی میکند:
- Domain Eventهای Aggregate بارگذاری میشوند.
- State فعلی با Projection رویدادها بازسازی میشود تا برای تصمیم کسبوکاری قابل استفاده باشد.
- Command اجرا میشود و منطق کسبوکار Domain Eventهای جدید تولید میکند.
- رویدادهای جدید در Event Store Commit میشوند.
در مثال Ticket فصل ۶، Application Service رویدادها را Load، Aggregate را Rehydrate، Command را اجرا و تغییرات را Persist میکند:
01 public class TicketAPI
02 {
03 private ITicketsRepository _ticketsRepository;
04 ...
05
06 public void RequestEscalation(TicketId id, EscalationReason reason)
07 {
08 var events = _ticketsRepository.LoadEvents(id);
09 var ticket = new Ticket(events);
10 var originalVersion = ticket.Version;
11 var cmd = new RequestEscalation(reason);
12 ticket.Execute(cmd);
13 _ticketsRepository.CommitChanges(ticket, originalVersion);
14 }
15
16 ...
17 }
Constructor، نمونهٔ TicketState را میسازد و برای هر Event، AppendEvent را فراخوانی میکند:
18 public class Ticket
19 {
20 ...
21 private List<DomainEvent> _domainEvents = new List<DomainEvent>();
22 private TicketState _state;
23 ...
24
25 public Ticket(IEnumerable<IDomainEvents> events)
26 {
27 _state = new TicketState();
28 foreach (var e in events)
29 {
30 AppendEvent(e);
31 }
32 }
AppendEvent رویداد را به منطق Projection میدهد:
33 private void AppendEvent(IDomainEvent @event)
34 {
35 _domainEvents.Append(@event);
36 // Dynamically call the correct overload of the "Apply" method.
37 ((dynamic)state).Apply((dynamic)@event);
38 }
برخلاف فصل قبل، RequestEscalation مستقیماً IsEscalated را true نمیکند؛ بلکه Event مناسب را میسازد و به AppendEvent میدهد:
39 public void Execute(RequestEscalation cmd)
40 {
41 if (!_state.IsEscalated && _state.RemainingTimePercentage <= 0)
42 {
43 var escalatedEvent = new TicketEscalated(_id, cmd.Reason);
44 AppendEvent(escalatedEvent);
45 }
46 }
47
48 ...
49 }
تمام Eventها سپس در TicketState اعمال و فیلدهای State مطابق دادهٔ رویداد تغییر میکنند:
50 public class TicketState
51 {
52 public TicketId Id { get; private set; }
53 public int Version { get; private set; }
54 public bool IsEscalated { get; private set; }
55 ...
56 public void Apply(TicketInitialized @event)
57 {
58 Id = @event.Id;
59 Version = 0;
60 IsEscalated = false;
61 ....
62 }
63
64 public void Apply(TicketEscalated @event)
65 {
66 IsEscalated = true;
67 Version += 1;
68 }
69
70 ...
71 }
چرا «Event-Sourced Domain Model»؟
استفاده از Event برای نمایش گذارهای State، یعنی Event Sourcing، با یا بدون بلوکهای Domain Model ممکن است. اصطلاح طولانیتر صریح میکند که در اینجا Event Sourcing برای نمایش تغییرات چرخهٔ عمر Aggregateهای Domain Model استفاده میشود.
مزایا
در مقایسه با ذخیرهٔ مستقیم State فعلی، Event-Sourced Domain Model برای مدلکردن Aggregate تلاش بیشتری میطلبد، اما مزایای مهمی دارد:
- سفر در زمان: همان Eventهایی که State فعلی را میسازند میتوانند تمام Stateهای گذشته را نیز بازسازی کنند. این قابلیت برای تحلیل رفتار سیستم، بررسی تصمیمها، بهینهسازی منطق و Retroactive Debugging مفید است؛ میتوان Aggregate را دقیقاً به لحظهٔ وقوع Bug برگرداند.
- بینش عمیق: Event Sourcing دادهٔ غنی از رفتار سیستم فراهم میکند و Projectionهای جدید را میتوان در آینده روی Eventهای موجود ساخت تا بینشهای تازه به دست آید.
- Audit Log: Domain Eventهای Persistشده یک Audit Log با Strong Consistency از تمام اتفاقات State Aggregate هستند. بعضی دامنهها قانوناً به چنین Logی نیاز دارند و در سیستمهای مالی نیز ردیابی تصمیم و جریان پول را بسیار آسان میکند.
- Optimistic Concurrency پیشرفته: بهجای اینکه فقط بفهمیم داده Stale شده، میتوان Eventهای دقیقِ اضافهشده میان Read و Write را دید و بر اساس قواعد کسبوکار تصمیم گرفت آیا واقعاً با عملیات جدید تعارض دارند یا میتوان ادامه داد.
معایب
- منحنی یادگیری: این الگو با روش سنتی مدیریت داده تفاوت زیادی دارد. تیم باید آموزش ببیند و به شیوهٔ فکری جدید عادت کند.
- تکامل مدل: Eventها طبق تعریف Immutable هستند. تغییر Schema رویداد مانند تغییر Schema جدول ساده نیست و Versioning سیستم Event-Sourced خود موضوعی گسترده است.
- پیچیدگی معماری: Event Sourcing قطعات متحرک معماری بیشتری اضافه میکند. فصل بعد با CQRS جزئیات بیشتری را بررسی میکند.
اگر مسئله با طراحی سادهتر حل شود، همهٔ این چالشها شدیدتر به چشم میآیند. فصل ۱۰ قواعدی برای انتخاب الگوی مناسب ارائه میکند.
پرسشهای پرتکرار
کارایی
آیا بازسازی State از Eventها با زیاد شدن Eventها Performance را خراب نمیکند؟
Projection رویدادها قدرت پردازشی میخواهد و با افزایش تعداد Eventها هزینه بیشتر میشود. باید تأثیر صدها یا هزاران Event Benchmark و با طول عمر مورد انتظار Aggregate مقایسه شود. در بیشتر سامانهها افت محسوس معمولاً پس از بیش از ۱۰٬۰۰۰ Event برای هر Aggregate رخ میدهد، در حالی که در اکثریت سیستمها طول عمر متوسط Aggregate از حدود ۱۰۰ Event فراتر نمیرود.
در موارد نادر که Projection واقعاً مسئلهٔ Performance شود، میتوان از Snapshot استفاده کرد؛ شکل ۷-۲. Process رویدادهای جدید را دنبال و Projection را در Cache ذخیره میکند. برای اجرای Action، Snapshot فعلی و فقط Eventهای پس از Version آن Fetch و روی Snapshot اعمال میشوند.
شکل ۷-۲ — Snapshot کردن Eventهای Aggregate.
Snapshot یک Optimization است و باید توجیه شود. اگر Aggregateها به ۱۰٬۰۰۰+ Event نمیرسند، پیادهسازی آن فقط پیچیدگی تصادفی است. پیش از آن بهتر است مرز Aggregate دوباره بررسی شود.
آیا حجم عظیم داده مقیاسپذیر است؟
مدل Event-Sourced بهراحتی Scale میشود، چون تمام عملیات هر Aggregate در Context همان Aggregate انجام میشود. Event Store را میتوان با Aggregate ID شارد کرد تا تمام Eventهای یک Instance در یک Shard باشند؛ شکل ۷-۳.
شکل ۷-۳ — Sharding در Event Store.
حذف داده
Event Store Append-only است؛ اگر برای GDPR واقعاً مجبور به حذف فیزیکی داده باشیم چه؟
میتوان از الگوی Forgettable Payload استفاده کرد: اطلاعات حساس در Eventها بهصورت رمزشده ذخیره و Key رمز در Key–Value Store جداگانه نگهداری میشود؛ Key برابر Aggregate ID است. برای حذف اطلاعات حساس، Key حذف میشود و دادهٔ رمزشده دیگر قابل دسترسی نیست.
چرا فقط Log متنی ننویسم؟
نوشتن همزمان در Operational Database و فایل Log، تراکنشی روی دو Storage است و مستعد خطاست. اگر Database Transaction شکست بخورد، معمولاً Log قبلی Rollback نمیشود؛ بنابراین Log Strongly Consistent نیست.
چرا State-based بمانم و در همان تراکنش Log Table را هم پر کنم؟
از نظر زیرساختی این کار State و Log را هماهنگ نگه میدارد، اما همچنان ممکن است مهندس آینده فراموش کند رکورد Log مناسب را اضافه کند. همچنین وقتی State منبع حقیقت است، Schema جدول Log معمولاً سریعاً به آشفتگی تبدیل میشود و تضمینی نیست همهٔ اطلاعات لازم با فرمت صحیح نوشته شوند.
چرا Trigger پایگاه داده History Table نسازد؟
Trigger نیاز به فراخوانی دستی را حذف میکند، اما History حاصل فقط «واقعیت خشک» تغییر فیلدها را ثبت میکند و زمینهٔ کسبوکاریِ چرای تغییر را از دست میدهد. نبود «چرا» توان ساخت Projectionهای اضافی را بهشدت محدود میکند.
جمعبندی
در این فصل Event Sourcing و کاربرد آن برای مدلکردن بُعد زمان در Aggregateهای Domain Model بررسی شد.
در Event-Sourced Domain Model تمام تغییرات State به صورت مجموعهای از Domain Eventها بیان میشوند؛ برخلاف مدل سنتی که تغییر State فقط یک رکورد را Update میکند. Eventها میتوانند State فعلی Aggregate را Projection کنند و همچنین چند نمایش متفاوت برای کارهای گوناگون بسازند.
این الگو برای موقعیتهایی مناسب است که بینش عمیق دربارهٔ دادهٔ سیستم برای تحلیل و بهینهسازی حیاتی است یا Audit Log قانوناً لازم است. با این فصل بررسی روشهای مدلسازی و پیادهسازی منطق کسبوکار کامل میشود. فصل بعد به سطح بالاتر، یعنی الگوهای معماری، میرود.
تمرینها
- کدام عبارت دربارهٔ رابطهٔ Domain Event و Value Object درست است؟
- Domain Event برای توصیف آنچه رخ داده از Value Objectها استفاده میکند.
- در Event-Sourced Domain Model باید Value Objectها به Event-Sourced Aggregate تبدیل شوند.
- Value Object فقط برای Domain Model است و در Event-Sourced Domain Model با Event جایگزین میشود.
- همهٔ عبارات نادرستاند.
- کدام عبارت دربارهٔ Projection State از Eventها درست است؟
- فقط یک نمایش State قابل Projection است.
- چند نمایش ممکن است اما Eventها باید از ابتدا برای آن طراحی شده باشند.
- چند نمایش ممکن است و در آینده هم میتوان Projectionهای جدید اضافه کرد.
- همه نادرستاند.
- کدام عبارت دربارهٔ تفاوت Aggregate مبتنی بر State و Event-Sourced درست است؟
- فقط Event-Sourced Aggregate میتواند Domain Event بسازد.
- هر دو Domain Event تولید میکنند اما فقط Event-Sourced آنها را Source of Truth قرار میدهد.
- Event-Sourced Aggregate تضمین میکند برای هر State Transition رویداد ایجاد شود.
- B و C درستاند.
- با رجوع به WolfDesk در پیشگفتار، کدام قابلیت سامانه برای Event-Sourced Domain Model مناسب است؟
شکلها و جدولهای منبع
برای حفظ نمایش بصری دقیق عناصر گرافیکی، بازنمایی صفحههای دارای شکل یا جدول منبع در ادامه آمده است. متن آموزشی فصل در بخشهای بالا به فارسی ترجمه شده است.
بازنمایی صفحهٔ 126 منبع برای حفظ دقیق شکل/جدول اصلی. Table 7-1. State-based model
بازنمایی صفحهٔ 133 منبع برای حفظ دقیق شکل/جدول اصلی. Figure 7-1. Event-sourced aggregate
بازنمایی صفحهٔ 139 منبع برای حفظ دقیق شکل/جدول اصلی. Figure 7-2. Snapshotting an aggregate’s events
بازنمایی صفحهٔ 140 منبع برای حفظ دقیق شکل/جدول اصلی. Figure 7-3. Sharding the event store