پرش به مطلب اصلی

زیرساخت دستور: راهنمای پیاده‌سازی

نمای کلی

زیرساخت دستور یک روش استاندارد برای ماژول‌ها فراهم می‌کند تا عملیات را فعال کنند یا وضعیت را تغییر دهند در ماژول‌های دیگر بدون ایجاد وابستگی شدید. برخلاف پرس‌وجوها (که داده می‌خوانند)، دستورها از معماری رویداد-محور برای حفظ جداسازی سست استفاده می‌کنند.

اصل اصلی

معماری رویداد-محور: ماژول‌ها با انتشار رویدادها به جای فراخوانی مستقیم سرویس‌های یکدیگر ارتباط برقرار می‌کنند. این یک الگوی انتشار-اشتراک ایجاد می‌کند که در آن ناشران نمی‌دانند چه کسی (اگر کسی باشد) گوش می‌دهد.

تفکیک دستور و پرس‌وجو (CQS)

تفکیک دغدغه‌ها

سیستم به شدت جدا می‌کند:

  • پرس‌وجوها: خواندن داده، برگرداندن DTOهای پیچیده، همزمان
  • دستورها/رویدادها: تغییر وضعیت، برگرداندن داده حداقلی (void/ID/boolean)، ناهمزمان

اجزای معماری

1. رویدادهای کسب‌وکار

رویدادها نمایانگر وقایع معنادار کسب‌وکار هستند که ماژول‌های دیگر ممکن است به آن‌ها اهمیت دهند.

ویژگی‌های رویداد
  • تغییرناپذیر (همه ویژگی‌ها readonly)
  • شامل داده حداقلی (IDها، نه موجودیت‌های کامل)
  • نام‌گذاری در زمان گذشته (OrderCompleted، UserRegistered)
  • پیاده‌سازی رابط نشانگر برای ایمنی نوع

2. شنوندگان رویداد

شنوندگان به رویدادها واکنش نشان می‌دهند و کار را به سرویس‌ها تفویض می‌کنند.

ویژگی‌های شنونده
  • پیاده‌سازی ShouldQueue برای پردازش ناهمزمان
  • هماهنگ‌کننده‌های نازک (تفویض به سرویس‌ها)
  • بدون منطق کسب‌وکار پیچیده
  • مدیریت یک نوع رویداد

3. گذرگاه رویداد

سیستم رویداد Laravel به عنوان گذرگاه پیام عمل می‌کند.

مسئولیت‌ها:

  • مسیریابی رویدادها به شنوندگان ثبت‌شده
  • مدیریت صف‌بندی برای شنوندگان ناهمزمان
  • فراهم کردن قلاب‌های چرخه حیات رویداد

ارتباط رویداد-محور

چرا رویدادها به جای فراخوانی مستقیم؟

OrderService.php - وابستگی شدید
class OrderService
{
public function __construct(
private InvoiceService $invoiceService, // وابستگی مستقیم
private NotificationService $notificationService, // وابستگی مستقیم
private AnalyticsService $analyticsService, // وابستگی مستقیم
) {}

public function completeOrder(int $orderId): void
{
// منطق تکمیل سفارش
$order = $this->orderRepository->find($orderId);
$order->status = 'completed';
$order->save();

// وابستگی شدید به ماژول‌های دیگر
$this->invoiceService->createInvoice($orderId);
$this->notificationService->sendOrderEmail($orderId);
$this->analyticsService->trackOrderCompletion($orderId);
}
}
مشکلات
  • OrderService از همه ماژول‌های وابسته اطلاع دارد
  • افزودن قابلیت جدید نیاز به تغییر OrderService دارد
  • نمی‌توان OrderService را بدون همه وابستگی‌ها تست کرد
  • اصل باز/بسته را نقض می‌کند

جریان رویداد


پیاده‌سازی گام به گام

گام 1: ایجاد رویداد کسب‌وکار

app/Events/OrderCompletedEvent.php
<?php

declare(strict_types=1);

namespace App\Events;

use App\Contracts\Event;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

final class OrderCompletedEvent implements Event
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;

public function __construct(
public readonly int $orderId,
public readonly int $userId,
public readonly int $totalAmount,
public readonly string $orderNumber,
) {
}
}
نکات کلیدی
  • پیاده‌سازی رابط نشانگر Event
  • همه ویژگی‌ها public readonly (تغییرناپذیر)
  • استفاده از traitهای رویداد Laravel
  • فقط شامل داده‌های ضروری (IDها و انواع ابتدایی)
  • نام‌گذاری در زمان گذشته (توصیف آنچه اتفاق افتاده)
  • استفاده از final برای جلوگیری از وراثت

گام 2: ارسال رویداد از سرویس

app/Modules/Orders/Services/OrderService.php
<?php

declare(strict_types=1);

namespace App\Modules\Orders\Services;

use App\Events\OrderCompletedEvent;
use App\Modules\Orders\Repositories\OrderRepository;

final class OrderService
{
public function __construct(
private readonly OrderRepository $repository
) {
}

public function completeOrder(int $orderId): bool
{
try {
$order = $this->repository->findOrFail($orderId);

// انجام عملیات کسب‌وکار
$order->status = 'completed';
$order->completed_at = now();
$order->save();

// highlight-start
// ارسال رویداد بعد از عملیات موفق
event(new OrderCompletedEvent(
orderId: $order->id,
userId: $order->user_id,
totalAmount: $order->total_amount,
orderNumber: $order->order_number,
));
// highlight-end

return true;
} catch (\Exception $e) {
// ثبت خطا
logger()->error('Failed to complete order', [
'order_id' => $orderId,
'exception' => $e->getMessage(),
]);

return false;
}
}
}
مهم
  • رویداد را بعد از عملیات موفق ارسال کنید
  • فقط داده‌های ضروری را در رویداد قرار دهید
  • رویدادها را در تراکنش‌ها ارسال نکنید (بعد از commit ارسال کنید)
  • از تابع کمکی event() استفاده کنید

گام 3: ایجاد شنونده رویداد

app/Modules/Invoicing/Listeners/CreateInvoiceOnOrderCompletion.php
<?php

declare(strict_types=1);

namespace App\Modules\Invoicing\Listeners;

use App\Events\OrderCompletedEvent;
use App\Modules\Invoicing\Services\InvoiceService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

final class CreateInvoiceOnOrderCompletion implements ShouldQueue
{
use InteractsWithQueue;

/**
* تعداد دفعات تلاش برای اجرای job
*/
public int $tries = 3;

/**
* تعداد ثانیه‌های انتظار قبل از تلاش مجدد
*/
public int $backoff = 60;

public function __construct(
private readonly InvoiceService $invoiceService
) {
}

public function handle(OrderCompletedEvent $event): void
{
try {
// تفویض به لایه سرویس
$this->invoiceService->createInvoiceForOrder(
orderId: $event->orderId,
userId: $event->userId,
totalAmount: $event->totalAmount,
);

logger()->info('Invoice created for order', [
'order_id' => $event->orderId,
'order_number' => $event->orderNumber,
]);
} catch (\Exception $e) {
logger()->error('Failed to create invoice', [
'order_id' => $event->orderId,
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);

// پرتاب مجدد برای فعال‌سازی مکانیزم تلاش مجدد
throw $e;
}
}

/**
* مدیریت شکست job
*/
public function failed(OrderCompletedEvent $event, \Throwable $exception): void
{
logger()->critical('Invoice creation failed after all retries', [
'order_id' => $event->orderId,
'exception' => $exception->getMessage(),
]);

// می‌توان ادمین را مطلع کرد، هشدار ایجاد کرد و غیره
}
}
نکات کلیدی
  • پیاده‌سازی ShouldQueue برای پردازش ناهمزمان
  • تفویض به لایه سرویس (هماهنگ‌کننده نازک)
  • پیکربندی منطق تلاش مجدد ($tries، $backoff)
  • لاگ‌گذاری جامع خطاها
  • پیاده‌سازی متد failed() برای مدیریت شکست نهایی
  • استفاده از تزریق وابستگی برای سرویس‌ها

گام 4: ثبت شنونده

app/Providers/EventServiceProvider.php
<?php

declare(strict_types=1);

namespace App\Providers;

use App\Events\OrderCompletedEvent;
use App\Modules\Invoicing\Listeners\CreateInvoiceOnOrderCompletion;
use App\Modules\Notifications\Listeners\SendOrderCompletionEmail;
use App\Modules\Analytics\Listeners\TrackOrderCompletion;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
/**
* نگاشت شنونده رویداد برای برنامه
*/
protected $listen = [
OrderCompletedEvent::class => [
CreateInvoiceOnOrderCompletion::class,
SendOrderCompletionEmail::class,
TrackOrderCompletion::class,
],

// سایر نگاشت‌های رویداد...
];

/**
* ثبت هر رویدادی برای برنامه
*/
public function boot(): void
{
//
}
}
نکات ثبت
  • نگاشت رویدادها به شنوندگان در آرایه $listen
  • چندین شنونده می‌توانند به یک رویداد مشترک شوند
  • ترتیب شنوندگان در آرایه ترتیب اجرا را تضمین نمی‌کند (ناهمزمان)
  • می‌توان از کشف خودکار رویداد به جای ثبت دستی استفاده کرد

گام 5: ایجاد لایه سرویس

app/Modules/Invoicing/Services/InvoiceService.php
<?php

declare(strict_types=1);

namespace App\Modules\Invoicing\Services;

use App\Domains\Order\OrderQuery;
use App\Domains\Order\OrderId;
use App\Domains\User\UserQuery;
use App\Domains\User\UserId;
use App\Modules\Invoicing\Repositories\InvoiceRepository;
use Illuminate\Support\Facades\DB;

final class InvoiceService
{
public function __construct(
private readonly InvoiceRepository $repository,
private readonly OrderQuery $orderQuery,
private readonly UserQuery $userQuery,
) {
}

public function createInvoiceForOrder(
int $orderId,
int $userId,
int $totalAmount,
): int {
// دریافت داده اضافی با استفاده از پرس‌وجوها
$order = $this->orderQuery->getById(new OrderId($orderId));
$user = $this->userQuery->getById(new UserId($userId));

// ایجاد فاکتور در تراکنش
return DB::transaction(function () use ($order, $user, $totalAmount) {
$invoice = $this->repository->create([
'order_id' => $order->id->value,
'user_id' => $user->id->value,
'order_number' => $order->orderNumber,
'customer_name' => $user->fullName,
'total_amount' => $totalAmount,
'status' => 'pending',
'created_at' => now(),
]);

return $invoice->id;
});
}
}
نکات کلیدی
  • استفاده از رابط‌های پرس‌وجو برای دریافت داده اضافی
  • شامل منطق کسب‌وکار واقعی
  • استفاده از تراکنش‌ها برای یکپارچگی داده
  • برگرداندن داده ساده (ID، boolean و غیره)
  • بدون آگاهی از رویدادها

الگوهای استفاده

بهترین برای: عملیات fire-and-forget که نیازی به تأیید ندارید.

class UserService
{
public function registerUser(array $data): int
{
$user = $this->repository->create($data);

// ارسال رویداد - منتظر نتیجه نمانید
event(new UserRegisteredEvent(
userId: $user->id,
email: $user->email,
));

return $user->id;
}
}

بهترین شیوه‌ها

1. نام‌گذاری رویداد

// زمان گذشته - توصیف آنچه اتفاق افتاده
class OrderCompletedEvent implements Event {}
class UserRegisteredEvent implements Event {}
class PaymentProcessedEvent implements Event {}
class InvoiceGeneratedEvent implements Event {}

2. داده رویداد

class OrderCompletedEvent implements Event
{
public function __construct(
public readonly int $orderId, // IDها
public readonly int $userId, // IDها
public readonly int $totalAmount, // انواع ابتدایی
public readonly string $orderNumber, // انواع ابتدایی
) {}
}

3. مسئولیت‌های شنونده

class CreateInvoiceListener implements ShouldQueue
{
public function handle(OrderCompletedEvent $event): void
{
// هماهنگ‌کننده نازک - تفویض به سرویس
$this->invoiceService->createInvoice($event->orderId);
}
}

4. مدیریت خطا

class ProcessPaymentListener implements ShouldQueue
{
public int $tries = 3;
public int $backoff = 60;

public function handle(OrderCompletedEvent $event): void
{
try {
$this->paymentService->process($event->orderId);
} catch (\Exception $e) {
logger()->error('Payment processing failed', [
'order_id' => $event->orderId,
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);

throw $e; // پرتاب مجدد برای تلاش مجدد
}
}

public function failed(OrderCompletedEvent $event, \Throwable $exception): void
{
// مدیریت شکست نهایی
logger()->critical('Payment failed after all retries', [
'order_id' => $event->orderId,
]);
}
}

5. همزمان در مقابل ناهمزمان

چه زمانی از هر کدام استفاده کنیم

ناهمزمان (توصیه می‌شود):

class SendEmailListener implements ShouldQueue  // ناهمزمان
{
public function handle(UserRegisteredEvent $event): void
{
$this->emailService->sendWelcomeEmail($event->userId);
}
}

از ناهمزمان استفاده کنید وقتی:

  • ارسال ایمیل
  • فراخوانی APIهای خارجی
  • پردازش سنگین
  • عملیات غیرحیاتی
  • اکثر موارد استفاده

همزمان (با احتیاط استفاده کنید):

class UpdateCacheListener  // همزمان - بدون ShouldQueue
{
public function handle(ProductUpdatedEvent $event): void
{
// باید فوراً اتفاق بیفتد
Cache::forget("product:{$event->productId}");
}
}

از همزمان استفاده کنید وقتی:

  • ابطال کش
  • سازگاری داده حیاتی
  • باید قبل از پاسخ کامل شود
  • عملیات بسیار سریع (<100ms)

6. دانه‌بندی رویداد

class OrderCompletedEvent implements Event {}
class OrderCancelledEvent implements Event {}
class OrderRefundedEvent implements Event {}

چرا: رویدادهای خاص درک و نگهداری آسان‌تری دارند.


استراتژی‌های تست

تست واحد شنوندگان

tests/Unit/Modules/Invoicing/Listeners/CreateInvoiceOnOrderCompletionTest.php
<?php

namespace Tests\Unit\Modules\Invoicing\Listeners;

use App\Events\OrderCompletedEvent;
use App\Modules\Invoicing\Listeners\CreateInvoiceOnOrderCompletion;
use App\Modules\Invoicing\Services\InvoiceService;
use Mockery;
use Tests\TestCase;

final class CreateInvoiceOnOrderCompletionTest extends TestCase
{
public function test_creates_invoice_when_order_completed(): void
{
// آماده‌سازی
$event = new OrderCompletedEvent(
orderId: 1,
userId: 10,
totalAmount: 10000,
orderNumber: 'ORD-001',
);

$invoiceService = Mockery::mock(InvoiceService::class);
$invoiceService->shouldReceive('createInvoiceForOrder')
->once()
->with(1, 10, 10000)
->andReturn(100); // شناسه فاکتور

$listener = new CreateInvoiceOnOrderCompletion($invoiceService);

// اجرا
$listener->handle($event);

// بررسی - تأیید شده توسط انتظارات Mockery
}

public function test_logs_error_on_failure(): void
{
// آماده‌سازی
$event = new OrderCompletedEvent(
orderId: 1,
userId: 10,
totalAmount: 10000,
orderNumber: 'ORD-001',
);

$invoiceService = Mockery::mock(InvoiceService::class);
$invoiceService->shouldReceive('createInvoiceForOrder')
->once()
->andThrow(new \Exception('Database error'));

$listener = new CreateInvoiceOnOrderCompletion($invoiceService);

// اجرا و بررسی
$this->expectException(\Exception::class);
$listener->handle($event);
}
}

تست یکپارچگی رویدادها

tests/Integration/Events/OrderCompletedEventTest.php
<?php

namespace Tests\Integration\Events;

use App\Events\OrderCompletedEvent;
use App\Modules\Invoicing\Listeners\CreateInvoiceOnOrderCompletion;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;

final class OrderCompletedEventTest extends TestCase
{
use RefreshDatabase;

public function test_event_dispatches_to_listeners(): void
{
// جعلی کردن رویدادها برای گرفتن آن‌ها
Event::fake([OrderCompletedEvent::class]);

// فعال‌سازی رویداد
event(new OrderCompletedEvent(
orderId: 1,
userId: 10,
totalAmount: 10000,
orderNumber: 'ORD-001',
));

// بررسی ارسال رویداد
Event::assertDispatched(OrderCompletedEvent::class, function ($event) {
return $event->orderId === 1
&& $event->userId === 10
&& $event->totalAmount === 10000;
});
}

public function test_listener_is_queued(): void
{
// جعلی کردن صف
Queue::fake();

// ارسال رویداد
event(new OrderCompletedEvent(
orderId: 1,
userId: 10,
totalAmount: 10000,
orderNumber: 'ORD-001',
));

// بررسی صف‌بندی شنونده
Queue::assertPushed(CreateInvoiceOnOrderCompletion::class);
}
}

الگوهای رایج و مثال‌ها

الگو: هماهنگی Saga/گردش کار

مورد استفاده: فرآیند چندمرحله‌ای در سراسر ماژول‌ها.

مشاهده کد پیاده‌سازی
// مرحله 1: سفارش تکمیل شد
class OrderService
{
public function completeOrder(int $orderId): void
{
$order = $this->repository->findOrFail($orderId);
$order->status = 'completed';
$order->save();

event(new OrderCompletedEvent($orderId));
}
}

// مرحله 2: فاکتور ایجاد شد
class CreateInvoiceListener implements ShouldQueue
{
public function handle(OrderCompletedEvent $event): void
{
$invoiceId = $this->invoiceService->create($event->orderId);

event(new InvoiceCreatedEvent($invoiceId, $event->orderId));
}
}

// مرحله 3: پرداخت پردازش شد
class ProcessPaymentListener implements ShouldQueue
{
public function handle(InvoiceCreatedEvent $event): void
{
$paymentId = $this->paymentService->process($event->invoiceId);

event(new PaymentProcessedEvent($paymentId, $event->invoiceId));
}
}

// مرحله 4: ارسال تأیید
class SendConfirmationListener implements ShouldQueue
{
public function handle(PaymentProcessedEvent $event): void
{
$this->emailService->sendConfirmation($event->paymentId);
}
}

الگو: تراکنش‌های جبرانی

مورد استفاده: بازگشت در صورت شکست.

class PaymentProcessedEvent implements Event
{
public function __construct(
public readonly int $paymentId,
public readonly int $orderId,
public readonly bool $success,
) {}
}

class RollbackOrderOnPaymentFailure implements ShouldQueue
{
public function handle(PaymentProcessedEvent $event): void
{
if (!$event->success) {
// تراکنش جبرانی
$this->orderService->revertToPending($event->orderId);

logger()->warning('Order reverted due to payment failure', [
'order_id' => $event->orderId,
'payment_id' => $event->paymentId,
]);
}
}
}

الگو: نسخه‌بندی رویداد

مورد استفاده: حفظ سازگاری با نسخه‌های قبلی.

// رویداد V1
class OrderCompletedEventV1 implements Event
{
public function __construct(
public readonly int $orderId,
public readonly int $totalAmount,
) {}
}

// رویداد V2 (با داده اضافی)
class OrderCompletedEventV2 implements Event
{
public function __construct(
public readonly int $orderId,
public readonly int $userId,
public readonly int $totalAmount,
public readonly string $orderNumber,
) {}
}

// شنونده آداپتور
class OrderEventAdapter implements ShouldQueue
{
public function handle(OrderCompletedEventV1 $event): void
{
// تبدیل V1 به V2
$order = $this->orderQuery->getById(new OrderId($event->orderId));

event(new OrderCompletedEventV2(
orderId: $event->orderId,
userId: $order->userId->value,
totalAmount: $event->totalAmount,
orderNumber: $order->orderNumber,
));
}
}

عیب‌یابی

مشکل: شنونده اجرا نمی‌شود

علائم
  • رویداد ارسال می‌شود اما شنونده اجرا نمی‌شود
  • هیچ خطایی در لاگ‌ها نیست
راه‌حل‌ها

1. بررسی ثبت:

EventServiceProvider.php
protected $listen = [
OrderCompletedEvent::class => [
CreateInvoiceListener::class, // اطمینان از ثبت
],
];

2. پاک کردن کش رویداد:

php artisan event:clear
php artisan cache:clear
php artisan config:clear

3. بررسی Queue Worker:

# اگر شنونده ShouldQueue را پیاده‌سازی می‌کند
php artisan queue:work

# بررسی jobهای شکست‌خورده
php artisan queue:failed

مشکل: رویداد چندین بار ارسال می‌شود

علائم
  • شنونده چندین بار برای یک رویداد اجرا می‌شود
  • فاکتورها/ایمیل‌های تکراری ایجاد می‌شوند
راه‌حل‌ها

1. بررسی ارسال‌های متعدد:

// چندین بار ارسال می‌شود
public function completeOrder(int $orderId): void
{
$order = $this->repository->find($orderId);
event(new OrderCompletedEvent($orderId)); // اول

$order->status = 'completed';
$order->save();

event(new OrderCompletedEvent($orderId)); // دوم - تکراری!
}

2. پیاده‌سازی Idempotency:

class CreateInvoiceListener implements ShouldQueue
{
public function handle(OrderCompletedEvent $event): void
{
// بررسی اینکه آیا فاکتور از قبل وجود دارد
if ($this->invoiceRepository->existsForOrder($event->orderId)) {
logger()->info('Invoice already exists, skipping', [
'order_id' => $event->orderId,
]);
return;
}

$this->invoiceService->create($event->orderId);
}
}

مشکل: مشکلات عملکرد

علائم
  • پردازش کند رویداد
  • رشد صف انتظار
  • استفاده بالای حافظه
راه‌حل‌ها

1. بهینه‌سازی شنونده:

// بارگذاری همه داده‌ها
class ProcessOrderListener implements ShouldQueue
{
public function handle(OrderCompletedEvent $event): void
{
$order = Order::with('items', 'user', 'payments')->find($event->orderId);
// پردازش...
}
}

2. استفاده از Chunking برای عملیات دسته‌ای:

class ProcessBulkOrdersListener implements ShouldQueue
{
public function handle(BulkOrdersCompletedEvent $event): void
{
// پردازش در قطعات
collect($event->orderIds)
->chunk(100)
->each(function ($chunk) {
$this->service->processBatch($chunk->toArray());
});
}
}

3. افزایش Queue Workerها:

پیکربندی Supervisor
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
numprocs=8 # افزایش تعداد workerها

نتیجه‌گیری

زیرساخت دستور/رویداد یک روش قوی برای فعال‌سازی عملیات در مرزهای ماژول در عین حفظ جداسازی سست فراهم می‌کند. با پیروی از این راهنمای پیاده‌سازی و بهترین شیوه‌ها، می‌توانید:

دستاوردها
  • ✅ به استقلال واقعی ماژول دست یابید
  • ✅ پردازش ناهمزمان را فعال کنید
  • ✅ از گردش کارهای پیچیده پشتیبانی کنید
  • ✅ قابلیت مشاهده سیستم را حفظ کنید
  • ✅ با صف‌ها به صورت افقی مقیاس‌بندی کنید

نکات کلیدی

  1. از رویدادها برای عملیات نوشتن استفاده کنید: دستورها/رویدادها وضعیت را تغییر می‌دهند، پرس‌وجوها داده می‌خوانند
  2. رویدادها را ساده نگه دارید: فقط IDها و انواع ابتدایی، نه موجودیت‌ها
  3. شنوندگان هماهنگ‌کننده هستند: به سرویس‌ها تفویض کنید، منطق کسب‌وکار نداشته باشید
  4. به صورت پیش‌فرض ناهمزمان: از ShouldQueue استفاده کنید مگر دلیل خاصی داشته باشید
  5. شکست‌ها را مدیریت کنید: منطق تلاش مجدد و متدهای failed() را پیاده‌سازی کنید
  6. همه چیز را لاگ کنید: لاگ‌گذاری جامع برای اشکال‌زدایی و مانیتورینگ
  7. به طور کامل تست کنید: تست واحد شنوندگان، تست یکپارچگی جریان رویداد
به یاد داشته باشید

رویدادها نمایانگر آنچه اتفاق افتاده است، نه آنچه باید اتفاق بیفتد.