تخطَّ إلى المحتوى

🟩 شرح Node.js

العمارة المعتمدة على الأحداث

الدرس 29 من 29· ⏱ 7 دقائق قراءة

مقدمة

العمارة المعتمدة على الأحداث (Event-Driven Architecture) هي نمط معماري يعتمد على إنتاج واكتشاف واستهلاك الأحداث. Node.js بطبيعته غير المتزامن والقائم على الأحداث هو بيئة مثالية لهذه العمارة.

مبادئ EDA — Event-Driven Architecture

المبادئ الأساسية

// مكون منتج للحدث (Producer)
class OrderService extends EventEmitter {
  async placeOrder(orderData) {
    const order = await db.orders.create(orderData);
    this.emit('order:placed', {
      orderId: order.id,
      userId: order.userId,
      total: order.total,
      timestamp: new Date()
    });
    return order;
  }
}

// مكون مستهلك للحدث (Consumer)
class NotificationService {
  constructor(eventBus) {
    eventBus.on('order:placed', this.handleOrderPlaced.bind(this));
  }

  async handleOrderPlaced(event) {
    await emailClient.send({
      to: event.userId,
      subject: 'تم تأكيد طلبك',
      template: 'order_confirmation',
      data: { orderId: event.orderId, total: event.total }
    });
  }
}

// مكون مستهلك آخر
class InventoryService {
  constructor(eventBus) {
    eventBus.on('order:placed', this.handleOrderPlaced.bind(this));
  }

  async handleOrderPlaced(event) {
    await inventoryClient.reserveItems(event.orderId);
  }
}

EventEmitter — الأنماط المتقدمة

معالجة الأخطاء

const EventEmitter = require('events');

class SafeEmitter extends EventEmitter {
  emit(type, ...args) {
    try {
      super.emit(type, ...args);
    } catch (err) {
      super.emit('error', err);
    }
    return this;
  }
}

const emitter = new SafeEmitter();

// مستمع آمن
emitter.on('data', async (payload) => {
  throw new Error('فشل المعالجة');
});

emitter.on('error', (err) => {
  console.error('خطأ في معالج حدث:', err.message);
  // تسجيل الخطأ في نظام المراقبة
});

emitter.emit('data', { id: 1 });

Memory Leaks — تسرب الذاكرة

const EventEmitter = require('events');

class EventBus extends EventEmitter {
  constructor() {
    super();
    this.setMaxListeners(100);
  }
}

const bus = new EventBus();

// ❌ خطأ: تسرب ذاكرة - إضافة مستمع جديد كل استدعاء
function processOrder(orderId) {
  bus.on('payment:complete', (payment) => {
    if (payment.orderId === orderId) {
      console.log('تم الدفع للطلب', orderId);
    }
  });
}

// ✅ صحيح: استخدام مستمع واحد مع معرّف
function processOrderFixed(orderId) {
  const handler = (payment) => {
    if (payment.orderId === orderId) {
      console.log('تم الدفع للطلب', orderId);
      bus.off('payment:complete', handler); // تنظيف
    }
  };
  bus.on('payment:complete', handler);
}

maxListeners

const EventEmitter = require('events');

// افتراضيًا: 10 مستمعين كحد أقصى للحدث الواحد
const emitter = new EventEmitter();
console.log(emitter.getMaxListeners()); // 10

// زيادة الحد
emitter.setMaxListeners(50);

// أو تعطيل التحذير
emitter.setMaxListeners(0);

Event Sourcing — تتبّع الأحداث

بدلاً من تخزين الحالة الحالية فقط، نخزن سلسلة الأحداث التي أدّت إلى هذه الحالة:

class EventStore {
  constructor() {
    this.events = [];
  }

  append(streamId, event) {
    this.events.push({
      streamId,
      ...event,
      eventId: `${streamId}-${this.events.length + 1}`,
      timestamp: new Date()
    });
  }

  getStream(streamId) {
    return this.events.filter(e => e.streamId === streamId);
  }

  replay(streamId, initialState = {}) {
    const events = this.getStream(streamId);
    return events.reduce((state, event) => {
      switch (event.type) {
        case 'ACCOUNT_CREATED':
          return { ...state, id: event.accountId, balance: 0, status: 'active' };
        case 'MONEY_DEPOSITED':
          return { ...state, balance: state.balance + event.amount };
        case 'MONEY_WITHDRAWN':
          return { ...state, balance: state.balance - event.amount };
        case 'ACCOUNT_CLOSED':
          return { ...state, status: 'closed' };
        default:
          return state;
      }
    }, initialState);
  }
}

const store = new EventStore();
store.append('account-1', { type: 'ACCOUNT_CREATED', accountId: 'account-1' });
store.append('account-1', { type: 'MONEY_DEPOSITED', amount: 1000 });
store.append('account-1', { type: 'MONEY_WITHDRAWN', amount: 200 });

const state = store.replay('account-1');
console.log(state); // { id: 'account-1', balance: 800, status: 'active' }

Domain Events — أحداث المجال

أحداث تعبّر عن شيء مهم حدث في domain التطبيق:

class DomainEvent {
  constructor(name, data) {
    this.name = name;
    this.data = data;
    this.id = crypto.randomUUID();
    this.occurredOn = new Date();
  }
}

class Account extends EventEmitter {
  constructor(id, owner) {
    super();
    this.id = id;
    this.owner = owner;
    this.balance = 0;
  }

  deposit(amount) {
    if (amount <= 0) throw new Error('المبلغ يجب أن يكون موجبًا');
    this.balance += amount;
    this.emit(new DomainEvent('money.deposited', {
      accountId: this.id,
      amount,
      newBalance: this.balance
    }));
    return this.balance;
  }

  withdraw(amount) {
    if (amount > this.balance) {
      this.emit(new DomainEvent('withdrawal.failed.insufficientFunds', {
        accountId: this.id,
        amount,
        balance: this.balance
      }));
      throw new Error('رصيد غير كافٍ');
    }
    this.balance -= amount;
    this.emit(new DomainEvent('money.withdrawn', {
      accountId: this.id,
      amount,
      newBalance: this.balance
    }));
    return this.balance;
  }
}

const account = new Account('acc-1', 'سارة');
account.on('domainEvent', (event) => {
  console.log(`حدث: ${event.name}`, event.data);
});
account.deposit(500);
account.withdraw(100);

CQRS مع الأحداث

CQRS (Command Query Responsibility Segregation) يفصل عمليات القراءة عن الكتابة:

// Command Side (يكتب الأحداث)
class OrderCommandHandler {
  constructor(eventStore) {
    this.eventStore = eventStore;
  }

  async createOrder(command) {
    const event = {
      type: 'ORDER_CREATED',
      data: {
        orderId: command.orderId,
        customerId: command.customerId,
        items: command.items,
        total: command.total
      }
    };
    await this.eventStore.append(`order-${command.orderId}`, event);
    return event;
  }

  async shipOrder(command) {
    const stream = await this.eventStore.getStream(`order-${command.orderId}`);
    const orderState = this.replay(stream);

    if (orderState.status !== 'confirmed') {
      throw new Error('يجب تأكيد الطلب قبل الشحن');
    }

    await this.eventStore.append(`order-${command.orderId}`, {
      type: 'ORDER_SHIPPED',
      data: { orderId: command.orderId, trackingNumber: command.trackingNumber }
    });
  }
}

// Query Side (يقرأ من التوقعات المحدثة)
class OrderProjection {
  constructor() {
    this.orders = new Map();
  }

  applyEvent(event) {
    const { orderId } = event.data;
    let order = this.orders.get(orderId) || { id: orderId, status: 'pending' };

    switch (event.type) {
      case 'ORDER_CREATED':
        order = { ...order, ...event.data, status: 'created' };
        break;
      case 'ORDER_CONFIRMED':
        order.status = 'confirmed';
        break;
      case 'ORDER_SHIPPED':
        order.status = 'shipped';
        order.trackingNumber = event.data.trackingNumber;
        break;
      case 'ORDER_DELIVERED':
        order.status = 'delivered';
        break;
    }

    this.orders.set(orderId, order);
  }
}

Message Queues — تكامل قوائم الرسائل

RabbitMQ

const amqp = require('amqplib');

class RabbitMQEventBus {
  constructor(url) {
    this.url = url;
    this.channel = null;
  }

  async connect() {
    const conn = await amqp.connect(this.url);
    this.channel = await conn.createChannel();
    await this.channel.assertExchange('events', 'topic', { durable: true });
  }

  async publish(routingKey, event) {
    await this.channel.publish(
      'events',
      routingKey,
      Buffer.from(JSON.stringify(event)),
      { persistent: true }
    );
  }

  async subscribe(routingKey, handler) {
    const q = await this.channel.assertQueue('', { exclusive: true });
    await this.channel.bindQueue(q.queue, 'events', routingKey);
    this.channel.consume(q.queue, (msg) => {
      const event = JSON.parse(msg.content.toString());
      handler(event);
      this.channel.ack(msg);
    });
  }
}

Redis Pub/Sub

const Redis = require('ioredis');

class RedisEventBus {
  constructor() {
    this.publisher = new Redis();
    this.subscriber = new Redis();
  }

  publish(channel, event) {
    return this.publisher.publish(channel, JSON.stringify(event));
  }

  subscribe(channel, handler) {
    this.subscriber.subscribe(channel);
    this.subscriber.on('message', (ch, message) => {
      if (ch === channel) {
        handler(JSON.parse(message));
      }
    });
  }
}

Error Handling — معالجة الأخطاء في الأنظمة الحدثية

class ResilientEventBus extends EventEmitter {
  constructor(options = {}) {
    super();
    this.retryCount = options.retryCount || 3;
    this.retryDelay = options.retryDelay || 1000;
    this.deadLetterQueue = [];
  }

  emit(type, ...args) {
    const event = { type, args, timestamp: Date.now() };
    this._processWithRetry(event, 0);
    return this;
  }

  async _processWithRetry(event, attempt) {
    try {
      const listeners = this.listeners(event.type);
      for (const listener of listeners) {
        await listener(...event.args);
      }
    } catch (err) {
      if (attempt < this.retryCount) {
        console.warn(`إعادة محاولة ${event.type} (${attempt + 1}/${this.retryCount})`);
        setTimeout(() => this._processWithRetry(event, attempt + 1), this.retryDelay);
      } else {
        console.error(`فشل دائم للحدث ${event.type}:`, err);
        this.deadLetterQueue.push({ event, error: err.message });
        super.emit('deadLetter', event, err);
      }
    }
  }
}

Testing — اختبار الكود الحدثي

const assert = require('assert');

// اختبار باستخدام Mock EventBus
class MockEventBus {
  constructor() {
    this.events = [];
  }

  emit(type, data) {
    this.events.push({ type, data });
  }

  getEvents() {
    return this.events;
  }

  clear() {
    this.events = [];
  }
}

async function testOrderFlow() {
  const eventBus = new MockEventBus();
  const orderService = new OrderService(eventBus);

  await orderService.placeOrder({ userId: 1, total: 500 });

  const events = eventBus.getEvents();
  assert.strictEqual(events.length, 1);
  assert.strictEqual(events[0].type, 'order:placed');
  assert.strictEqual(events[0].data.total, 500);
  assert.ok(events[0].data.orderId);

  console.log('✅ اختبار تدفق الطلب passed');
}

⚠️ تحذير: الأنظمة الحدثية الموزعة تزيد من التعقيد. ابدأ بـ EventEmitter بسيط، ثم انتقل إلى message queues عند الحاجة إلى الصمود والتوسّع.

خلاصة

  • Event-Driven Architecture: تفصل المكونات عبر الأحداث، مما يزيد من المرونة وقابلية التوسّع.
  • EventEmitter: قلب Node.js للأحداث، مع الانتباه إلى الـ memory leaks و maxListeners.
  • Event Sourcing: يخزن سلسلة الأحداث بدلاً من الحالة النهائية.
  • Domain Events: أحداث تعبّر عن أشياء مهمة في منطق الأعمال.
  • CQRS: يفصل أوامر الكتابة عن استعلامات القراءة.
  • Message Queues: توفّر تواصل موثوق بين الخدمات الموزعة (RabbitMQ، Redis).
  • اختبر الكود الحدثي باستخدام Mocks للتحقق من صحة تدفق الأحداث.

🎯 هذا هو الدرس الأخير في مسار Node.js. تهانينا على إكمال المسار!

شرح العمارة المعتمدة على الأحداث — Node.js بالعربي
العمارة المعتمدة على الأحداثNode.js بالعربي · The Code Fix

📚 لمزيد من التعمّق في Node.js، راجِع التوثيق الرسمي لـ Node.js.

هل كان هذا الدرس مفيدًا؟