في مجال هندسة البرمجيات الحديثة، غالباً ما تواجه تطبيقات CRUD التقليدية (إنشاء، قراءة، تحديث، حذف) عقبات عند التعامل مع منطق الأعمال المعقد ومتطلبات التدقيق الصارمة. هنا يأتي دور Event Sourcing، ليس كنمط فحسب، بل كتغيير جذري في النموذج الفكري. من خلال فصل الحالة الحالية للنظام عن سجل التغييرات، يمكن للمطورين بناء أنظمة أكثر شفافية ومرونة ومتانة.
ما هو Event Sourcing؟
في جوهره، يُعد Event Sourcing نمطاً معمارياً حيث لا يتم تخزين حالة التطبيق مباشرة، بل يتم استنتاجها من تسلسل من الأحداث. بدلاً من حفظ الحالة الحالية للكائن (على سبيل المثال، OrderStatus = Shipped)، تقوم بحفظ حقيقة وقوع حدث معين (على سبيل المثال، OrderShipped).
تخيل حساباً بنكياً. في النظام التقليدي، قد تقوم بتحديث عمود الرصيد في سجل قاعدة البيانات. أما في النظام المعتمد على Event Sourcing، فلديك دفتر حسابات يسجل كل إيداع وسحب. لمعرفة الرصيد الحالي، تقوم ببساطة بجمع هذه الأحداث. يحول هذا النهج منطق تطبيقك إلى سجل غير قابل للتعديل (Append-only)، مما يوفر مسار تدقيق كاملاً بشكل افتراضي.
لماذا تختار Event Sourcing؟
الفائدة الرئيسية لـ Event Sourcing هي القدرة على إعادة بناء حالة الكيان في أي نقطة زمنية. هذا أمر لا يقدر بثمن في تصحيح الأخطاء، والتدقيق، والامتثال التنظيمي. علاوة على ذلك، يتماشى هذا النهج بشكل طبيعي مع التصميم المتمحور حول المجال (DDD)، حيث تمثل الأحداث غالباً نقاط تحول أعمال هامة.
ومع ذلك، فهو ليس خالياً من التعقيد. تصبح استعلامات البيانات أكثر تعقيداً لأن مخزن الأحداث (Event Store) مُحسّن للكتابة وليس للقراءة. ولهذا السبب غالباً ما يُقترن Event Sourcing مع CQRS (فصل مسؤولية الأوامر والاستعلامات). تستخدم مخزن الأحداث للكتابة وقاعدة بيانات منفصلة مُحسّنة للقراءة (مثل مخزن SQL أو NoSQL) للاستعلامات.
تنفيذ Event Sourcing: مثال برمجي
لنلقِ نظرة على تنفيذ مبسط بلغة Java لتوضيح كيفية استنتاج الحالة من الأحداث. سنقوم بإنشاء جذر تجميعي (Aggregate Root) بسيط يدير طلباً.
public class Order {
private String orderId;
private OrderStatus status;
private List<ObjectEvent> events = new ArrayList<>();
// Constructor
public Order(String orderId) {
this.orderId = orderId;
this.status = OrderStatus.CREATED;
// Record the initial event
applyEvent(new OrderCreatedEvent(orderId));
}
// Command handler
public void ship() {
if (this.status != OrderStatus.PACKED) {
throw new IllegalStateException("Order must be packed before shipping");
}
applyEvent(new OrderShippedEvent(orderId, Instant.now()));
}
// Apply an event to the current state
private void applyEvent(ObjectEvent event) {
events.add(event);
event.apply(this);
}
// Rebuild state from events (used when loading from Event Store)
public void replay(List<ObjectEvent> historicalEvents) {
this.events = new ArrayList<>();
for (ObjectEvent event : historicalEvents) {
applyEvent(event);
}
}
}
// Interface for all events
interface ObjectEvent {
void apply(Order order);
}
// Example Event
class OrderShippedEvent implements ObjectEvent {
private String orderId;
private Instant shippedAt;
public OrderShippedEvent(String orderId, Instant shippedAt) {
this.orderId = orderId;
this.shippedAt = shippedAt;
}
@Override
public void apply(Order order) {
order.status = OrderStatus.SHIPPED;
System.out.println("Order " + orderId + " shipped at " + shippedAt);
}
}
في هذا المثال، لا تحتفظ فئة Order بسجلها التاريخي. فهي تحتفظ فقط بالحالة الحالية. عندما نحتاج إلى تحميل الطلب من التخزين الدائم، نمرر القائمة التاريخية للأحداث إلى طريقة replay، مما يسمح للكائن بإعادة بناء نفسه بدقة.
التحديات وأفضل الممارسات
على الرغم من قوته، فإن Event Sourcing يطرح تحديات. يعد إدارة الإصدارات أمراً بالغ الأهمية؛ فإذا تغير مخطط الأحداث الخاص بك، يجب عليك التعامل مع التوافق مع الإصدارات السابقة. بالإضافة إلى ذلك، قد تتأثر الأداء إذا كنت تقوم بإعادة تشغيل آلاف الأحداث بشكل متكرر. للتخفيف من ذلك، غالباً ما يقوم المطورون بتنفيذ لقطات (Snapshots) - أي حفظ حالة التجميع بشكل دوري لتجنب إعادة تشغيل الأحداث القديمة.
الخاتمة
يُعد Event Sourcing أداة قوية لمجالات المشاكل المحددة، خاصة تلك التي تتطلب سلامة عالية، وسجلاً تاريخياً معقداً، ومرونة في التقارير. وعلى الرغم من أنه يضيف عبئاً إلى عملية التطوير، فإن الفوائد طويلة المدى المتمثلة في امتلاك سجل كامل وغير قابل للتعديل لنطاق عملك غالباً ما تفوق التكاليف. من خلال فهم هذا النمط وتطبيقه، يمكن للمعماريين بناء أنظمة ليست وظيفية فحسب، بل غنية أيضاً بالرؤى العميقة.