إدارة منطق الأعمال باستخدام تدفقات الأحداث: تطبيقات مرنة وقابلة للتتبع مع مصدر الأحداث
مقدمة: لماذا نسجل الأحداث وليس مجرد الحالة؟
عادةً ما تقوم أساليب قواعد البيانات التقليدية بتخزين الحالة الحالية للنظام. عندما يقوم المستخدم بإجراء ما، يتم تحديث البيانات في الجدول ذي الصلة، وتضيع الحالة القديمة. ومع ذلك، في العديد من السيناريوهات، يكون معرفة سبب وكيفية تغيير شيء ما أكثر قيمة من مجرد الحالة الحالية. هنا يأتي دور نمط مصدر الأحداث (Event Sourcing). يسجل هذا النمط كل تغيير في الحالة كحدث ويعيد بناء الحالة الحالية للنظام كتراكم لهذه الأحداث.
المبادئ الأساسية لمصدر الأحداث
جوهر مصدر الأحداث هو أن كل معاملة تجارية يتم تسجيلها كـ 'حدث'. هذه الأحداث هي سجلات غير قابلة للتغيير تمثل تغييرًا في النظام ويتم إلحاقها بمخزن الأحداث (Event Store). يتم اشتقاق الحالة الحالية عن طريق تطبيق هذه الأحداث بالتسلسل.
- الأحداث غير القابلة للتغيير: بمجرد إنشاء الحدث، لا يمكن أبدًا تغييره أو حذفه.
- مخزن الأحداث: قاعدة بيانات يتم فيها تخزين جميع الأحداث بترتيب زمني.
- إعادة بناء الحالة: يمكن دائمًا إعادة بناء الحالة الحالية للكائن عن طريق إعادة تشغيل جميع الأحداث ذات الصلة بالتسلسل.
المزايا وحالات الاستخدام
- قابلية التدقيق الكاملة: يوفر سجلًا كاملاً لكل تغيير في النظام.
- الاستعلام الزمني: يمكنك بسهولة الاستعلام عن حالة النظام في أي وقت (على سبيل المثال، رصيد البنك قبل ثلاثة أيام).
- تصحيح الأخطاء والتراجع: يمكن فحص تدفقات الأحداث لتحديد المشكلات أو التراجع عن التغييرات غير المرغوب فيها.
- التكامل مع CQRS: يتوافق بشكل طبيعي مع نمط فصل مسؤوليات الأوامر والاستعلامات (CQRS)، ويفصل نماذج القراءة والكتابة.
- تحليل الأعمال الأكثر ثراءً: يتيح فهمًا أفضل لأسباب وتأثيرات التغييرات في عمليات الأعمال.
مثال عملي: حساب بنكي بسيط
دعنا ننمذج تطبيق حساب بنكي باستخدام مصدر الأحداث. سيتم تسجيل إنشاء الحساب وإيداعات الأموال وعمليات السحب كأحداث.
1. تعريفات الأحداث
سيكون كل حدث عبارة عن فئة تحتوي على إجراء محدد وبيانات ذات صلة.
public interface IEvent {}public class AccountCreatedEvent : IEvent{ public Guid AccountId { get; } public string Owner { get; } public DateTime CreatedAt { get; } public AccountCreatedEvent(Guid accountId, string owner, DateTime createdAt) { AccountId = accountId; Owner = owner; CreatedAt = createdAt; }}public class MoneyDepositedEvent : IEvent{ public Guid AccountId { get; } public decimal Amount { get; } public DateTime DepositedAt { get; } public MoneyDepositedEvent(Guid accountId, decimal amount, DateTime depositedAt) { AccountId = accountId; Amount = amount; DepositedAt = depositedAt; }}public class MoneyWithdrawnEvent : IEvent{ public Guid AccountId { get; } public decimal Amount { get; } public DateTime WithdrawnAt { get; } public MoneyWithdrawnEvent(Guid accountId, decimal amount, DateTime withdrawnAt) { AccountId = accountId; Amount = amount; WithdrawnAt = withdrawnAt; }}2. التجميع (Aggregate) وتطبيق الأحداث
تستقبل مجموعة BankAccount الأوامر، وتولد الأحداث، ثم تطبق هذه الأحداث على حالتها الخاصة.
public class BankAccount{ public Guid Id { get; private set; } public string Owner { get; private set; } public decimal Balance { get; private set; } private List<IEvent> _changes = new List<IEvent>(); public BankAccount() { Balance = 0; } public static BankAccount CreateNew(Guid accountId, string owner) { var account = new BankAccount(); account.ApplyChange(new AccountCreatedEvent(accountId, owner, DateTime.UtcNow)); return account; } public void Deposit(decimal amount) { if (amount <= 0) throw new ArgumentException("يجب أن يكون مبلغ الإيداع أكبر من الصفر."); ApplyChange(new MoneyDepositedEvent(Id, amount, DateTime.UtcNow)); } public void Withdraw(decimal amount) { if (amount <= 0) throw new ArgumentException("يجب أن يكون مبلغ السحب أكبر من الصفر."); if (Balance < amount) throw new InvalidOperationException("رصيد غير كاف."); ApplyChange(new MoneyWithdrawnEvent(Id, amount, DateTime.UtcNow)); } public IEnumerable<IEvent> GetUncommittedChanges() => _changes; public void MarkChangesAsCommitted() => _changes.Clear(); public void LoadFromHistory(IEnumerable<IEvent> history) { foreach (var e in history) { ApplyChange(e, isNew: false); } } private void ApplyChange(IEvent e, bool isNew = true) { When((dynamic)e); if (isNew) { _changes.Add(e); } } private void When(AccountCreatedEvent e) { Id = e.AccountId; Owner = e.Owner; Balance = 0; } private void When(MoneyDepositedEvent e) { Balance += e.Amount; } private void When(MoneyWithdrawnEvent e) { Balance -= e.Amount; }}3. مخزن الأحداث (Event Store)
سيكون مخزن الأحداث قادرًا على حفظ الأحداث وإعادة تشغيل الأحداث لمجموعة ما.
public interface IEventStore{ void SaveEvents(Guid aggregateId, IEnumerable<IEvent> events, int expectedVersion); List<IEvent> GetEventsForAggregate(Guid aggregateId);}// تطبيق بسيط لمخزن الأحداث في الذاكرةpublic class InMemoryEventStore : IEventStore{ private readonly Dictionary<Guid, List<IEvent>> _storage = new Dictionary<Guid, List<IEvent>>(); public void SaveEvents(Guid aggregateId, IEnumerable<IEvent> events, int expectedVersion) { if (!_storage.ContainsKey(aggregateId)) { _storage.Add(aggregateId, new List<IEvent>()); } var existingEvents = _storage[aggregateId]; // Optimistic concurrency check if (expectedVersion != -1 && existingEvents.Count != expectedVersion) { throw new InvalidOperationException("تعارض بالتزامن. الإصدار المتوقع: " + expectedVersion + ", الإصدار الحالي: " + existingEvents.Count); } existingEvents.AddRange(events); } public List<IEvent> GetEventsForAggregate(Guid aggregateId) { return _storage.ContainsKey(aggregateId) ? _storage[aggregateId] : new List<IEvent>(); }}4. تنفيذ الأحداث وإعادة بناء الحالة
// منطق التطبيق (مثال على الاستخدام)var eventStore = new InMemoryEventStore();var accountId = Guid.NewGuid();var newAccount = BankAccount.CreateNew(accountId, "أحمد سعيد");newAccount.Deposit(100);newAccount.Withdraw(30);eventStore.SaveEvents(accountId, newAccount.GetUncommittedChanges(), -1);newAccount.MarkChangesAsCommitted();// تحميل الحساب من مكان آخر وإجراء العمليات عليهvar events = eventStore.GetEventsForAggregate(accountId);var existingAccount = new BankAccount();existingAccount.LoadFromHistory(events);Console.WriteLine($"مالك الحساب: {existingAccount.Owner}, الرصيد: {existingAccount.Balance}"); // الإخراج: مالك الحساب: أحمد سعيد, الرصيد: 70existingAccount.Deposit(50);eventStore.SaveEvents(accountId, existingAccount.GetUncommittedChanges(), events.Count);existingAccount.MarkChangesAsCommitted();var updatedEvents = eventStore.GetEventsForAggregate(accountId);var updatedAccount = new BankAccount();updatedAccount.LoadFromHistory(updatedEvents);Console.WriteLine($"مالك الحساب المحدث: {updatedAccount.Owner}, الرصيد المحدث: {updatedAccount.Balance}"); // الإخراج: مالك الحساب المحدث: أحمد سعيد, الرصيد المحدث: 120الخلاصة
يوفر مصدر الأحداث منظورًا مختلفًا تمامًا لهندسة البرمجيات. من خلال الاحتفاظ بسلسلة من الأحداث التي تشكل كل حالة، بدلاً من مجرد لقطات للحالات، يمكننا أن نجعل أنظمتنا أكثر شفافية وقابلية للتدقيق ومرونة. على الرغم من وجود منحنى تعليمي أولي، يوفر مصدر الأحداث أداة قوية للمطورين، خاصة في سيناريوهات منطق الأعمال المعقدة، حيث تكون البيانات التاريخية حاسمة، وفي هندسة الخدمات المصغرة. يتيح لك هذا النمط فهم ليس فقط 'كيف' تبدو أنظمتك، ولكن أيضًا 'لماذا' تبدو بهذه الطريقة.
التعليقات (0)
لا توجد تعليقات بعد. كن أول من يعلق!