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

🏗️ شرح المايكروسيرفيس (Microservices)

نمط CQRS

الدرس 10 من 25· ⏱ 3 دقائق قراءة

ما هو CQRS؟

CQRS (Command Query Responsibility Segregation) يفصل بين أوامر الكتابة (Commands) واستعلامات القراءة (Queries).

الفكرة الأساسية

بدلاً من API واحد للقراءة والكتابة:

// قبل CQRS — API واحد
app.post('/users', createUser);
app.get('/users/:id', getUser);
app.patch('/users/:id', updateUser);
// بعد CQRS
const writeApi = express();
writeApi.post('/users', createUser);    // Command

const readApi = express();
readApi.get('/users/:id', getUser);    // Query

مثال متكامل

import { OrderRepositoryProtocol, InsufficientFundsError } from './lib/base_repository';

// Command (طلب تنفيذ إجراء)
class CreateOrderCommand {
  constructor(
    readonly userId: string,
    readonly items: OrderItem[],
    readonly idempotencyKey: string,
  ) {}
}

// Command Handler — يستخدم Repository، لا يستدعي db.orders.save() مباشرة
class OrderCommandHandler {
  constructor(
    private readonly orderRepo: OrderRepositoryProtocol,
    private readonly paymentRepo: PaymentRepositoryProtocol,
  ) {}

  async handle(command: CreateOrderCommand): Promise<OrderEntity> {
    const existing = await this.orderRepo.findById(command.idempotencyKey);
    if (existing) return existing; // Idempotency

    const total = command.items.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
    const balance = await this.paymentRepo.getBalance(command.userId);

    if (balance < total) {
      throw new InsufficientFundsError(balance, total);
    }

    const order: OrderEntity = {
      id: command.idempotencyKey,
      userId: command.userId,
      items: command.items,
      total,
      status: 'PENDING',
      createdAt: new Date(),
    };

    await this.orderRepo.save(order);
    await eventBus.publish('order.created', { orderId: order.id });
    return order;
  }
}
import { OrderNotFoundError, OrderRepositoryProtocol } from './lib/base_repository';

// Query Side (القراءة) — يستخدم Read Model Repository منفصل
class OrderQueryHandler {
  constructor(
    private readonly readRepo: OrderRepositoryProtocol,
  ) {}

  async getOrder(id: string): Promise<OrderReadModel> {
    const order = await this.readRepo.findById(id);
    if (!order) throw new OrderNotFoundError(id);

    return {
      id: order.id,
      userName: order.userName,
      items: order.items.map(i => ({
        name: i.productName,
        quantity: i.quantity,
        price: i.unitPrice,
      })),
      total: order.total,
      status: order.status,
    };
  }

  async getOrdersByUser(userId: string): Promise<OrderReadModel[]> {
    const orders = await this.readRepo.findByUserId(userId);
    return orders.map(o => ({
      id: o.id,
      userName: o.userName,
      items: o.items.map(i => ({
        name: i.productName,
        quantity: i.quantity,
        price: i.unitPrice,
      })),
      total: o.total,
      status: o.status,
    }));
  }
}

متى نستخدم CQRS؟

  • قراءة وكتابة بنماذج مختلفة
  • أحمال قراءة وكتابة غير متوازنة
  • الحاجة لأداء عالٍ للقراءة
  • فرق مختلفة تعمل على القراءة والكتابة

Read Model منفصل

import { OrderRepositoryProtocol } from './lib/base_repository';

// مزامنة Read Model مع Write Model عبر Events — يستخدم Repository
class ReadModelProjector {
  constructor(
    private readonly writeRepo: OrderRepositoryProtocol,
    private readonly readRepo: OrderRepositoryProtocol,
  ) {}

  async onOrderCreated(event: OrderCreatedEvent): Promise<void> {
    const order = await this.writeRepo.findById(event.orderId);
    if (!order) return;

    // Read Model منفصل — قد يكون بقاعدة بيانات مختلفة
    await this.readRepo.save({
      id: order.id,
      userId: order.userId,
      userName: order.userName,
      items: order.items,
      total: order.total,
      status: 'CREATED',
      createdAt: new Date(),
    });
  }
}

CQRS مع Event Sourcing

import { InsufficientFundsError } from './lib/base_repository';

// Aggregate Root مع Event Sourcing
class Account {
  private balance: number = 0;
  private readonly events: DomainEvent[] = [];

  apply(event: DomainEvent): void {
    if (event.type === 'DEPOSIT') this.balance += event.amount;
    if (event.type === 'WITHDRAW') {
      if (this.balance < event.amount) {
        throw new InsufficientFundsError(this.balance, event.amount);
      }
      this.balance -= event.amount;
    }
  }

  // Command — يتحقق من صحة العملية قبل تطبيقها
  deposit(amount: number): void {
    this.apply({ type: 'DEPOSIT', amount });
  }

  withdraw(amount: number): void {
    this.apply({ type: 'WITHDRAW', amount });
    // Event Store عبر Repository، ليس eventStore.save() مباشراً
  }

  // Query بسيط — يقرأ من الحالة الحالية
  getBalance(): number {
    return this.balance;
  }

  getUncommittedEvents(): readonly DomainEvent[] {
    return this.events;
  }
}

⚠️ CQRS يزيد التعقيد — طبقه فقط عندما يكون هناك فائدة حقيقية من الفصل

🎯 التالي: 10-saga

شرح نمط CQRS — المايكروسيرفيس (Microservices) بالعربي
نمط CQRSالمايكروسيرفيس (Microservices) بالعربي · The Code Fix

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