هندسة البرمجيات

نمط CQRS و Event Sourcing: التنفيذ مع أمثلة كود

دليل تنفيذ CQRS و Event Sourcing الكامل مع TypeScript و Node.js. يغطي مخازن الأحداث والإسقاطات واللقطات ومتى تستحق هذه الأنماط التعقيد.

Khalid Aboubakr
22 دقيقة قراءة
CqrsEvent SourcingDddArchitecture PatternsEventual ConsistencyEvent Store

مقدمة

CQRS (فصل مسؤولية الأوامر والاستعلامات) و Event Sourcing أنماط قوية يمكنها حل مشاكل محددة بأناقة. يمكنها أيضاً إضافة تعقيد هائل عند تطبيقها بشكل غير مناسب.

بعد تطبيق هذه الأنماط في أنظمة تدقيق الرعاية الصحية ومنصات المعاملات المالية وأنظمة الامتثال الحكومية، طورت فهماً واضحاً لمتى تتألق ومتى تكون مبالغة.

فهم CQRS

CQRS يفصل عمليات القراءة والكتابة إلى نماذج مختلفة:

// النهج التقليدي: نفس النموذج للقراءات والكتابات class OrderService { async createOrder(data: CreateOrderDto): Promise<Order> { const order = new Order(data); await this.orderRepository.save(order); return order; } async getOrdersWithDetails(userId: string): Promise<OrderWithDetails[]> { // غير مريح: يحتاج لربط جداول متعددة وتحويل البيانات const orders = await this.orderRepository.findByUserId(userId); return Promise.all(orders.map(async (order) => { // ... منطق تجميع معقد })); } }
// نهج CQRS: نماذج منفصلة محسنة لغرضها // جانب الكتابة: يفرض قواعد العمل class OrderCommandHandler { async handle(command: CreateOrderCommand): Promise<void> { const order = Order.create(command); await this.orderRepository.save(order); await this.eventPublisher.publish(order.pullEvents()); } } // جانب القراءة: محسن للاستعلامات class OrderQueryHandler { async getOrdersWithDetails(userId: string): Promise<OrderDetailsView[]> { // عرض مسبق الحساب وغير طبيعي محسن لهذا الاستعلام return this.orderDetailsViewRepository.findByUserId(userId); } }

فهم Event Sourcing

Event Sourcing يخزن الحالة كتسلسل من الأحداث بدلاً من الحالة الحالية:

// التقليدي: تخزين الحالة الحالية // جدول orders: { id, status, total_amount, customer_id, updated_at } // Event Sourcing: تخزين الأحداث // جدول order_events: // { stream_id, version, event_type, event_data, timestamp } class Order extends AggregateRoot { // إعادة بناء الحالة من الأحداث static fromEvents(events: DomainEvent[]): Order { const order = new Order(); events.forEach(event => order.apply(event)); return order; } private apply(event: DomainEvent): void { switch (event.type) { case 'OrderCreated': this.id = new OrderId(event.data.orderId); this.status = OrderStatus.PENDING; break; case 'ItemAdded': this.items.push(new OrderItem(event.data)); break; // ... المزيد من الأحداث } } }

متى تستخدم CQRS

السيناريو 1: عدم تماثل القراءة/الكتابة

عندما تفوق القراءات الكتابات بشكل كبير ولها خصائص أداء مختلفة:

// كتالوج منتجات التجارة الإلكترونية // الكتابات: ~100/ساعة (تحديثات المسؤول) // القراءات: ~100,000/ساعة (تصفح العملاء)

السيناريو 2: متطلبات استعلام معقدة

عندما لا يدعم نموذج النطاق بشكل طبيعي الاستعلامات المطلوبة.

متى تستخدم Event Sourcing

السيناريو 1: متطلبات التدقيق

عندما يجب عليك شرح ما حدث بالضبط ومتى:

// نظام الرعاية الصحية: مسار تدقيق كامل مطلوب بالتنظيم class PatientMedicationHistory { getFullHistory(patientId: PatientId): MedicationEvent[] { return this.eventStore.getEvents(patientId, 'medication'); // يرجع: // [ // { type: 'MedicationPrescribed', ... }, // { type: 'MedicationDispensed', ... }, // { type: 'AdverseReactionReported', ... } // ] } }

السيناريو 2: الاستعلامات الزمنية

عندما تحتاج للإجابة "ما كانت الحالة في الوقت X؟":

class AccountBalanceService { // ما كان الرصيد في 31 ديسمبر للتقرير الضريبي؟ getBalanceAtTime(accountId: AccountId, asOf: Date): Money { const events = this.eventStore.getEventsUntil(accountId, asOf); return events.reduce((balance, event) => { switch (event.type) { case 'Deposited': return balance.add(event.amount); case 'Withdrawn': return balance.subtract(event.amount); default: return balance; } }, Money.zero()); } }

السيناريو 3: منطق عمل معقد مع إعادات التشغيل

عندما تحتاج لإصلاح الأخطاء بإعادة تشغيل الأحداث بمنطق مصحح.

متى لا تستخدم هذه الأنماط

تطبيقات CRUD البسيطة

منشورات المدونة، ملفات المستخدمين، إدارة المخزون البسيطة - CRUD جيد تماماً!

الفرق الصغيرة بدون خبرة Event Sourcing

منحنى التعلم حاد. إذا لم يستخدم فريقك هذه الأنماط من قبل، ابدأ بنهج أبسط.

عندما يكون الاتساق القوي مطلوباً

Event sourcing و CQRS يقدمان الاتساق النهائي. إذا كان نطاقك يتطلب اتساقاً فورياً عبر جميع العروض، فكر ملياً.

اعتبارات التنفيذ

اختيار مخزن الأحداث

// الخيار 1: مبني لهذا الغرض (EventStoreDB) const eventStore = new EventStoreDBClient({ endpoint: 'localhost:2113' }); // الخيار 2: البناء على قاعدة بيانات موجودة class PostgresEventStore implements EventStore { async append(streamId: string, events: DomainEvent[]): Promise<void> { await this.db.transaction(async (tx) => { // ... منطق الإلحاق }); } }

اللقطات للأداء

class OrderRepository { async getById(orderId: OrderId): Promise<Order> { // التحقق من لقطة حديثة const snapshot = await this.snapshotStore.getLatest(orderId); // تحميل الأحداث منذ اللقطة const events = snapshot ? await this.eventStore.getEventsSince(orderId, snapshot.version) : await this.eventStore.getAllEvents(orderId); // إعادة البناء من اللقطة + الأحداث const order = snapshot ? Order.fromSnapshot(snapshot.state) : new Order(); events.forEach(event => order.apply(event)); return order; } }

الخلاصة

CQRS و Event Sourcing أدوات قوية تحل مشاكل حقيقية:

  • CQRS: عندما تختلف أنماط القراءة والكتابة بشكل كبير
  • Event Sourcing: عندما تكون مسارات التدقيق أو الاستعلامات الزمنية أو قدرات الإعادة مطلوبة

لكنها تأتي بتكاليف:

  • زيادة التعقيد
  • تحديات الاتساق النهائي
  • منحنى تعلم أكثر حدة
  • المزيد من البنية التحتية للإدارة

ابدأ بسيطاً. أضف هذه الأنماط فقط عندما يكون لديك متطلبات واضحة تبرر تعقيدها.

مقالات ذات صلة

هندسة البرمجياتقراءة 25 دقيقة

سياقات DDD المحددة: دليل التنفيذ الكامل

تعلم كيفية تنفيذ السياقات المحددة في التصميم المبني على النطاق. دليل عملي يغطي خرائط السياق والتجميعات وأحداث النطاق وأمثلة حقيقية.