مقدمة إلى معمارية المونوليث المعياري
تجمع معمارية المونوليث المعياري بين بساطة التطبيقات المونوليثية وفوائد التصميم المعياري التنظيمية. توفر مساراً نحو الخدمات الميكروية دون التعقيد الفوري للأنظمة الموزعة.
المفهوم الأساسي: ينظم المونوليث المعياري الكود في وحدات مستقلة بحدود واضحة، تمثل كل منها مجالاً تجارياً أو سياقاً محدوداً، مع الحفاظ على وحدة نشر واحدة.
لماذا المونوليث المعياري؟
غالباً ما تصبح التطبيقات المونوليثية التقليدية صعبة الصيانة مع نموها. تعالج معمارية المونوليث المعياري هذا من خلال:
- حدود واضحة: كل وحدة لها مسؤوليات وواجهات محددة جيداً
- تطوير مستقل: يمكن للفرق العمل على وحدات مختلفة بأقل قدر من التضارب
- اختبار أسهل: يمكن اختبار الوحدات بشكل منفصل
- مسار الترحيل: يمكن استخراج الوحدات إلى خدمات ميكروية عند الحاجة
- عمليات أبسط: وحدة نشر واحدة، قاعدة بيانات مشتركة، بدون زمن وصول الشبكة
بنية الوحدة في Laravel
ينظم مونوليث Laravel المعياري النموذجي الوحدات داخل دليل app/Modules:
app/
├── Modules/
│ ├── User/
│ │ ├── Models/
│ │ │ └── User.php
│ │ ├── Controllers/
│ │ │ ├── UserController.php
│ │ │ └── ProfileController.php
│ │ ├── Repositories/
│ │ │ └── UserRepository.php
│ │ ├── Services/
│ │ │ └── UserService.php
│ │ ├── Events/
│ │ │ └── UserRegistered.php
│ │ ├── Listeners/
│ │ │ └── SendWelcomeEmail.php
│ │ ├── Routes/
│ │ │ ├── api.php
│ │ │ └── web.php
│ │ ├── Database/
│ │ │ ├── Migrations/
│ │ │ └── Seeders/
│ │ ├── Tests/
│ │ │ ├── Unit/
│ │ │ └── Feature/
│ │ └── Providers/
│ │ └── UserServiceProvider.php
│ ├── Order/
│ │ └── ... (بنية مماثلة)
│ ├── Payment/
│ │ └── ... (بنية مماثلة)
│ └── Shared/
│ └── ... (أدوات مشتركة)
إنشاء مزود خدمة الوحدة
يجب أن يكون لكل وحدة مزود خدمة خاص بها لتسجيل المسارات والارتباطات والمستمعين:
<?php
namespace App\Modules\User\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Route;
class UserServiceProvider extends ServiceProvider
{
/**
* تسجيل خدمات الوحدة
*/
public function register(): void
{
// تسجيل مستودعات الوحدة
$this->app->bind(
'App\Modules\User\Repositories\UserRepositoryInterface',
'App\Modules\User\Repositories\UserRepository'
);
// تسجيل خدمات الوحدة
$this->app->singleton(
'App\Modules\User\Services\UserService'
);
}
/**
* تحميل خدمات الوحدة
*/
public function boot(): void
{
// تحميل ترحيلات الوحدة
$this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
// تحميل عروض الوحدة
$this->loadViewsFrom(__DIR__ . '/../Resources/views', 'user');
// تحميل ترجمات الوحدة
$this->loadTranslationsFrom(__DIR__ . '/../Resources/lang', 'user');
// تسجيل مسارات الوحدة
$this->registerRoutes();
// تسجيل مستمعي أحداث الوحدة
$this->registerEventListeners();
}
/**
* تسجيل مسارات الوحدة
*/
protected function registerRoutes(): void
{
Route::middleware('web')
->group(__DIR__ . '/../Routes/web.php');
Route::middleware('api')
->prefix('api')
->group(__DIR__ . '/../Routes/api.php');
}
/**
* تسجيل مستمعي الأحداث
*/
protected function registerEventListeners(): void
{
$this->app['events']->listen(
'App\Modules\User\Events\UserRegistered',
'App\Modules\User\Listeners\SendWelcomeEmail'
);
}
}
نصيحة احترافية: سجل جميع مزودي خدمات الوحدات في config/app.php أو استخدم الاكتشاف التلقائي باتباع اتفاقيات حزم Laravel.
التواصل بين الوحدات
يجب أن تتواصل الوحدات من خلال واجهات محددة جيداً للحفاظ على الاقتران الفضفاض. هناك عدة أنماط للتواصل بين الوحدات:
1. نمط عقد الخدمة
تحديد واجهات في النواة المشتركة يمكن للوحدات تنفيذها:
<?php
namespace App\Modules\Shared\Contracts;
interface NotificationServiceInterface
{
public function send(string $userId, string $message): void;
public function sendBatch(array $userIds, string $message): void;
}
// التنفيذ في وحدة الإشعارات
namespace App\Modules\Notification\Services;
use App\Modules\Shared\Contracts\NotificationServiceInterface;
class NotificationService implements NotificationServiceInterface
{
public function send(string $userId, string $message): void
{
// التنفيذ
}
public function sendBatch(array $userIds, string $message): void
{
// التنفيذ
}
}
// الاستخدام في وحدة الطلب
namespace App\Modules\Order\Services;
use App\Modules\Shared\Contracts\NotificationServiceInterface;
class OrderService
{
public function __construct(
private NotificationServiceInterface $notificationService
) {}
public function createOrder(array $data): Order
{
$order = Order::create($data);
// استخدام خدمة الإشعارات دون معرفة التنفيذ
$this->notificationService->send(
$order->user_id,
"تم إنشاء طلبك رقم #{$order->id}"
);
return $order;
}
}
2. التواصل القائم على الأحداث
استخدم أحداث Laravel للتواصل غير المتزامن والمفصول بين الوحدات:
<?php
// وحدة الطلب تنشر الحدث
namespace App\Modules\Order\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderPlaced
{
use Dispatchable, SerializesModels;
public function __construct(
public int $orderId,
public int $userId,
public float $amount
) {}
}
// وحدة الدفع تستمع للحدث
namespace App\Modules\Payment\Listeners;
use App\Modules\Order\Events\OrderPlaced;
class ProcessPayment
{
public function handle(OrderPlaced $event): void
{
// معالجة الدفع للطلب
// لا تحتاج هذه الوحدة لمعرفة تفاصيل وحدة الطلب الداخلية
}
}
// وحدة الإشعارات تستمع لنفس الحدث
namespace App\Modules\Notification\Listeners;
use App\Modules\Order\Events\OrderPlaced;
class SendOrderConfirmation
{
public function handle(OrderPlaced $event): void
{
// إرسال إشعار تأكيد الطلب
}
}
تحذير: تجنب التبعيات الدائرية بين الوحدات. إذا كانت الوحدة A تعتمد على الوحدة B، يجب ألا تعتمد الوحدة B أبداً على الوحدة A. استخدم الأحداث أو الواجهات المشتركة لكسر التبعيات الدائرية.
النواة المشتركة
تحتوي النواة المشتركة على كود تحتاج وحدات متعددة للوصول إليه. يجب أن تكون قليلة وتدار بعناية:
app/Modules/Shared/
├── Contracts/ # واجهات التواصل بين الوحدات
├── ValueObjects/ # كائنات القيمة المشتركة (Money, Email, إلخ)
├── Enums/ # التعدادات المشتركة
├── Exceptions/ # فئات الاستثناءات المشتركة
├── Traits/ # السمات المشتركة
└── Helpers/ # الدوال المساعدة المشتركة
مثال على كائن قيمة مشترك:
<?php
namespace App\Modules\Shared\ValueObjects;
class Money
{
private function __construct(
private float $amount,
private string $currency
) {}
public static function make(float $amount, string $currency = 'USD'): self
{
if ($amount < 0) {
throw new \InvalidArgumentException('لا يمكن أن يكون المبلغ سالباً');
}
return new self($amount, strtoupper($currency));
}
public function amount(): float
{
return $this->amount;
}
public function currency(): string
{
return $this->currency;
}
public function add(Money $other): self
{
if ($this->currency !== $other->currency) {
throw new \InvalidArgumentException('عدم تطابق العملة');
}
return new self($this->amount + $other->amount, $this->currency);
}
public function format(): string
{
return number_format($this->amount, 2) . ' ' . $this->currency;
}
}
السياقات المحدودة
تمثل كل وحدة سياقاً محدوداً من التصميم المدفوع بالمجال. يحدد السياق المحدود حدوداً واضحة يكون فيها النموذج قابلاً للتطبيق:
مثال: قد يكون لكيان "المستخدم" معانٍ مختلفة في سياقات مختلفة:
- وحدة المستخدم: تركز على المصادقة، الملف الشخصي، التفضيلات
- وحدة الطلب: تعرض المستخدم كمشترٍ بعناوين شحن
- وحدة الدعم: تعرض المستخدم كمنشئ تذكرة بسجل دعم
تحتفظ كل وحدة بتمثيلها الخاص لـ "المستخدم" المناسب لسياقها.
تمرين 1: إنشاء بنية وحدة
أنشئ وحدة "Product" بالبنية التالية:
- أنشئ بنية دليل الوحدة مع Models وControllers وServices وProviders
- أنشئ ProductServiceProvider يحمل المسارات ويسجل الارتباطات
- أنشئ نموذج Product والترحيل
- سجل مزود الخدمة في config/app.php
تمرين 2: تنفيذ التواصل بين الوحدات
نفذ التواصل بين وحدتي Order وInventory:
- أنشئ حدث OrderPlaced في وحدة Order
- أنشئ مستمع DecrementStock في وحدة Inventory
- سجل المستمع لمعالجة الحدث
- اختبر أن وضع طلب يقلل من المخزون
تمرين 3: إنشاء كائن قيمة مشترك
أنشئ كائن قيمة Email قابل لإعادة الاستخدام في النواة المشتركة:
- أنشئ فئة Email مع التحقق في المنشئ
- أضف الدوال: isValid() وdomain() وtoString()
- استخدم كائن القيمة Email في وحدتي User وNewsletter
- اكتب اختبارات لكائن القيمة Email
فوائد المونوليث المعياري
- قابلية التوسع للفريق: يمكن لعدة فرق العمل بشكل مستقل على وحدات مختلفة
- تنظيم الكود: الحدود الواضحة تجعل قاعدة الكود أسهل في التنقل
- الاختبار: يمكن اختبار الوحدات بشكل منفصل مع تبعيات وهمية
- الترحيل التدريجي: يمكن استخراج الوحدات إلى خدمات ميكروية عند الحاجة
- الأداء: لا يوجد عبء شبكة بين الوحدات كما في الخدمات الميكروية
- نشر مبسط: وحدة نشر واحدة تقلل من التعقيد التشغيلي
أفضل ممارسة: ابدأ بمونوليث معياري وانتقل فقط إلى الخدمات الميكروية عندما يكون لديك دليل واضح على أن الفوائد تفوق تكاليف التعقيد.