
التصميم الموجّه بالمجال (DDD): بناء البرمجيات حول منطق الأعمال
التصميم الموجّه بالمجال، والمعروف اختصاراً باسم DDD، هو أسلوب في تصميم البرمجيات يضع مجال العمل نفسه في مركز النظام. بدل أن يبدأ المطور من جداول قاعدة البيانات، أو الكنترولرات، أو بنية الإطار البرمجي، يبدأ DDD من المشكلة الحقيقية التي يحاول النظام حلها: الطلبات، المدفوعات، الفواتير، الاشتراكات، الشحن، الموافقات، السياسات، وقواعد العمل.
الفكرة الأساسية بسيطة لكنها قوية: يجب أن تتكلم البرمجية بلغة العمل. عندما يستخدم الكود نفس المفاهيم التي يستخدمها أصحاب العمل أو فريق المنتج، يصبح النظام أوضح، وأسهل في الصيانة، وأكثر أماناً عند التوسع والتعديل.
ما هو Domain-Driven Design؟
Domain-Driven Design هو منهجية تصميم تركّز على تمثيل منطق العمل المعقد داخل التطبيق بطريقة واضحة ومنظمة. اشتهر هذا المفهوم من خلال Eric Evans، وأصبح مهماً جداً في الأنظمة التي تكون فيها قواعد العمل أهم من مجرد تخزين البيانات وعرضها.
DDD ليس مجرد تقسيم مجلدات. وليس فقط إنشاء مجلدات باسم Domain وApplication وInfrastructure. الهدف الحقيقي من DDD هو جعل الكود يعكس الواقع التجاري أو العملي للنظام بأكبر قدر ممكن من الوضوح.
مثلاً، في نظام CRUD بسيط قد يكون لديك جدول اسمه orders وكنترولر يقوم بتحديث حالة الطلب. لكن في نظام حقيقي، تغيير حالة الطلب قد يكون مرتبطاً بعدة قواعد:
لا يمكن شحن الطلب قبل تأكيد الدفع.
لا يمكن إلغاء الطلب بعد شحنه.
لا يمكن إنشاء عملية استرداد إلا إذا تم تحصيل الدفع.
قد يمتلك العميل المميز قواعد إلغاء مختلفة.
يجب إنشاء فاتورة بعد نجاح عملية الدفع.
إذا كانت هذه القواعد موزعة بين الكنترولرات، والـ Jobs، والـ Observers، والـ Helpers، وحتى داخل استعلامات قاعدة البيانات، سيصبح النظام صعب الفهم والصيانة. DDD يحاول تنظيم هذه القواعد داخل نماذج أعمال واضحة لها معنى.
المشكلة الأساسية التي يحلها DDD
كثير من التطبيقات تبدأ بشكل نظيف، ثم تصبح معقدة ومربكة مع نمو المشروع. في البداية، يضع المطورون المنطق داخل الكنترولرات، أو الموديلات، أو الخدمات، أو حتى داخل الواجهات. هذا قد يعمل في المشاريع الصغيرة، لكنه يصبح خطيراً عندما تكبر قواعد العمل.
المشكلة الشائعة أن النظام يكون منظماً تقنياً، لكنه غير واضح من ناحية العمل. قد تجد Controllers وModels وMigrations وRequests وResources وJobs، لكن لا أحد يعرف بدقة أين تعيش قاعدة العمل الحقيقية.
مثال:
public function cancel($id)
{
$order = Order::findOrFail($id);
if ($order->status === 'shipped') {
return response()->json(['message' => 'Cannot cancel shipped order'], 422);
}
if ($order->payment_status === 'captured') {
Refund::create([
'order_id' => $order->id,
'amount' => $order->total,
]);
}
$order->status = 'cancelled';
$order->save();
return response()->json(['message' => 'Order cancelled']);
}هذا الكود قد يعمل، لكنه يخفي قاعدة العمل داخل الكنترولر. إذا احتجت نفس منطق الإلغاء من لوحة الإدارة، أو API، أو Command، أو Job، أو مهمة مجدولة، قد يتم تكرار المنطق. لاحقاً، عندما تتغير سياسة الإلغاء، قد يعدّل المطور مكاناً واحداً وينسى مكاناً آخر.
DDD يعالج هذه المشكلة بنقل قاعدة العمل إلى نموذج المجال نفسه.
DDD يبدأ من لغة العمل
من أهم مفاهيم DDD مفهوم Ubiquitous Language، ويعني أن يستخدم المطورون وأصحاب العمل نفس اللغة عند الحديث عن النظام.
إذا كان فريق العمل يستخدم كلمة “عميل”، فلا يجب أن يستخدم الكود كلمات مختلفة مثل “User” و“Client” و“Account” و“Profile” لنفس المفهوم بدون سبب واضح. وإذا كان العمل يتحدث عن “تجديد الاشتراك”، فلا يجب إخفاء هذا السلوك داخل دالة عامة اسمها updateStatus().
الكود الجيد في DDD يجعل سلوك العمل ظاهراً:
$order->cancel();
$invoice->markAsPaid();
$subscription->renew();
$shipment->dispatch();
$payment->capture();
$customer->upgradeToPremium();هذا النوع من الكود أسهل في القراءة لأنه يصف أفعالاً تجارية واضحة، وليس مجرد عمليات تقنية على بيانات.
Domain وSubdomain وسياق العمل
Domain هو مجال العمل الذي يُبنى النظام من أجله. في تطبيق تجارة إلكترونية، قد يكون المجال هو بيع المنتجات عبر الإنترنت. في نظام مستشفى، قد يشمل المجال المرضى، الأطباء، المواعيد، الوصفات الطبية، والفواتير. في منصة تعليمية، قد يشمل المجال الدورات، الدروس، الطلاب، الشهادات، والاشتراكات.
المجالات الكبيرة غالباً تُقسّم إلى Subdomains. كل Subdomain يمثل جزءاً محدداً من العمل.
مثال في نظام تجارة إلكترونية:
Catalog: المنتجات، التصنيفات، الأسعار، والتوفر.
Ordering: السلة، الطلبات، عناصر الطلب، وحالة الطلب.
Payment: تفويض الدفع، تحصيل الدفع، والاسترداد.
Shipping: إنشاء الشحنة، التتبع، والتسليم.
Customer Support: التذاكر، الشكاوى، الإرجاع، وخدمة العملاء.
يساعدك DDD على تجنب خلط كل هذه المفاهيم داخل نظام واحد ضخم، حيث تعرف كل كلاس أكثر مما يجب عن باقي أجزاء النظام.
Bounded Context: أهم مفهوم استراتيجي في DDD
Bounded Context هو حد واضح يكون فيه نموذج معين صالحاً ومفهوماً. نفس الكلمة قد تحمل معاني مختلفة في أجزاء مختلفة من العمل.
مثلاً، كلمة Customer قد تعني أشياء مختلفة:
في سياق المبيعات، العميل هو شخص يشتري المنتجات.
في سياق الدعم، العميل هو شخص يفتح تذاكر دعم.
في سياق الفوترة، العميل هو شخص لديه فواتير وطرق دفع.
محاولة إنشاء موديل واحد عالمي باسم Customer يخدم كل السياقات غالباً تؤدي إلى كائن ضخم ومربك. لذلك ينصح DDD بفصل النماذج حسب السياق.
بدل هذا:
App\Models\Customerيمكن أن يكون لديك:
App\Domain\Sales\Models\Customer
App\Domain\Support\Models\Customer
App\Domain\Billing\Models\Customerهذا لا يعني دائماً أنك تحتاج إلى جداول مختلفة في قاعدة البيانات. المقصود أن يكون نموذج الكود واضحاً حول سياق العمل الذي ينتمي إليه.
Entity في DDD
Entity هو كائن يمتلك هوية فريدة ويستمر كشيء واحد حتى لو تغيّرت خصائصه.
مثلاً، الطلب يعتبر Entity. قد تتغير حالته من pending إلى paid ثم shipped، لكنه يبقى نفس الطلب لأنه يمتلك نفس الهوية.
final class Order
{
private string $id;
private string $status;
private float $total;
public function __construct(string $id, float $total)
{
$this->id = $id;
$this->status = 'pending';
$this->total = $total;
}
public function pay(): void
{
if ($this->status !== 'pending') {
throw new DomainException('Only pending orders can be paid.');
}
$this->status = 'paid';
}
public function cancel(): void
{
if ($this->status === 'shipped') {
throw new DomainException('Shipped orders cannot be cancelled.');
}
$this->status = 'cancelled';
}
public function status(): string
{
return $this->status;
}
}النقطة المهمة هنا أن الـ Entity يحمي قواعده بنفسه. بدلاً من السماح لأي جزء في النظام بتغيير الحالة مباشرة، يوفّر الكائن أفعالاً ذات معنى مثل pay() وcancel().
Value Object في DDD
Value Object هو كائن يُعرّف من خلال قيمته، وليس من خلال هوية خاصة به. لا يحتاج إلى ID. إذا كان لدينا كائنان من نفس النوع ويحملان نفس القيم، فهما متساويان من ناحية المعنى.
أمثلة على Value Objects:
عنوان البريد الإلكتروني
المال
العنوان
نطاق زمني
نسبة مئوية
إحداثيات جغرافية
بدلاً من تمرير نصوص وأرقام خام في كل مكان، تجعل Value Objects الكود أوضح وأكثر أماناً.
final class Money
{
public function __construct(
private readonly int $amountInCents,
private readonly string $currency
) {
if ($amountInCents < 0) {
throw new InvalidArgumentException('Money amount cannot be negative.');
}
if ($currency === '') {
throw new InvalidArgumentException('Currency is required.');
}
}
public function amountInCents(): int
{
return $this->amountInCents;
}
public function currency(): string
{
return $this->currency;
}
public function add(Money $money): Money
{
if ($this->currency !== $money->currency) {
throw new DomainException('Cannot add money with different currencies.');
}
return new Money(
$this->amountInCents + $money->amountInCents,
$this->currency
);
}
}بدون Value Object، قد يتم تمثيل المال كـ float في مكان، وكـ integer في مكان آخر، وكنص في مكان ثالث. هذا يفتح الباب للأخطاء. وجود كائن مثل Money يعطي المفهوم شكلاً واضحاً وقواعد محددة.
Aggregate وAggregate Root
Aggregate هو مجموعة من الكائنات المرتبطة التي يجب الحفاظ على اتساقها معاً. أما Aggregate Root فهو الكائن الرئيسي الذي يتحكم في الوصول إلى هذه المجموعة.
مثلاً، الطلب قد يحتوي على عناصر طلب. في هذه الحالة، Order هو Aggregate Root، وعناصر الطلب OrderItem تابعة له داخل نفس الـ Aggregate.
غالباً لا يجب تعديل عناصر الطلب مباشرة من خارج الطلب. يجب أن يتحكم الطلب نفسه بكيفية إضافة العناصر أو حذفها أو تعديلها.
final class Order
{
private array $items = [];
public function addItem(string $productId, int $quantity, Money $price): void
{
if ($this->status !== 'pending') {
throw new DomainException('Cannot add items to a non-pending order.');
}
if ($quantity <= 0) {
throw new DomainException('Quantity must be greater than zero.');
}
$this->items[] = new OrderItem($productId, $quantity, $price);
}
public function total(): Money
{
$total = new Money(0, 'USD');
foreach ($this->items as $item) {
$total = $total->add($item->subtotal());
}
return $total;
}
}هذا يحمي اتساق العمل. الطلب هو من يقرر هل يمكن إضافة عنصر أم لا. والطلب هو من يحسب المجموع. والطلب هو من يحافظ على القواعد التي تخصه.
Repository في DDD
Repository مسؤول عن جلب وحفظ كائنات المجال. في DDD، يجب أن يبدو الـ Repository وكأنه مجموعة من كائنات المجال، وليس مجرد Helper للاستعلام من قاعدة البيانات.
طبقة المجال لا يجب أن تهتم هل البيانات تأتي من MySQL أو PostgreSQL أو MongoDB أو Redis أو API خارجي. هذه تفاصيل بنية تحتية.
واجهة Repository قد تكون بهذا الشكل:
interface OrderRepository
{
public function findById(string $id): ?Order;
public function save(Order $order): void;
}أما التنفيذ فقد يستخدم Laravel Eloquent، لكن المجال نفسه لا يجب أن يعتمد مباشرة على Eloquent.
final class EloquentOrderRepository implements OrderRepository
{
public function findById(string $id): ?Order
{
$record = OrderModel::with('items')->find($id);
if ($record === null) {
return null;
}
return OrderMapper::toDomain($record);
}
public function save(Order $order): void
{
$record = OrderMapper::toEloquent($order);
$record->save();
}
}هذا الفصل يجعل منطق العمل أسهل في الاختبار وأقل ارتباطاً بالإطار البرمجي.
Domain Service
Domain Service يحتوي على منطق عمل لا ينتمي بشكل طبيعي إلى Entity أو Value Object واحد.
مثلاً، حساب تكلفة الشحن قد يعتمد على وزن الطلب، عنوان العميل، طريقة التوصيل، ومناطق الشحن. إذا لم يكن هذا المنطق مناسباً بالكامل داخل Order أو Customer، يمكن وضعه داخل Domain Service.
final class ShippingCostCalculator
{
public function calculate(Order $order, Address $address): Money
{
if ($address->country() === 'TR') {
return new Money(5000, 'TRY');
}
if ($order->totalWeight() > 10000) {
return new Money(2500, 'USD');
}
return new Money(1500, 'USD');
}
}يجب أن يبقى Domain Service ممثلاً لمنطق عمل حقيقي. لا يجب أن يتحول إلى خدمة تقنية لإرسال الإيميلات، أو كتابة السجلات، أو الاتصال بالـ APIs. هذه الأمور مكانها في طبقة Infrastructure.
Application Service
Application Service ينسّق تنفيذ حالة استخدام معينة. لا يجب أن يحتوي على قواعد عمل عميقة. وظيفته استقبال البيانات، تحميل كائنات المجال، استدعاء سلوك المجال، حفظ التغييرات، وربما إطلاق أحداث أو عمليات جانبية.
مثال:
final class CancelOrderService
{
public function __construct(
private readonly OrderRepository $orders
) {
}
public function handle(string $orderId): void
{
$order = $this->orders->findById($orderId);
if ($order === null) {
throw new RuntimeException('Order not found.');
}
$order->cancel();
$this->orders->save($order);
}
}Application Service لا يقرر هل يمكن إلغاء الطلب أم لا. هذه القاعدة يجب أن تكون داخل نموذج المجال. الخدمة هنا فقط تنسّق العملية.
Domain Events
Domain Event يمثل شيئاً مهماً حدث داخل العمل.
أمثلة:
OrderWasPlaced
PaymentWasCaptured
InvoiceWasGenerated
SubscriptionWasRenewed
CustomerWasUpgraded
Domain Events تجعل النظام أكثر تعبيراً. بدلاً من وضع كل التأثيرات الجانبية داخل نفس الدالة، يمكن للمجال أن يسجل أن شيئاً مهماً حدث. وبعدها تتفاعل أجزاء أخرى من النظام مع هذا الحدث.
final class OrderWasPlaced
{
public function __construct(
public readonly string $orderId,
public readonly string $customerId
) {
}
}داخل الـ Aggregate:
final class Order
{
private array $events = [];
public function place(): void
{
if (empty($this->items)) {
throw new DomainException('Cannot place an empty order.');
}
$this->status = 'placed';
$this->events[] = new OrderWasPlaced(
$this->id,
$this->customerId
);
}
public function releaseEvents(): array
{
$events = $this->events;
$this->events = [];
return $events;
}
}بعد حفظ الطلب، يمكن لطبقة التطبيق نشر هذه الأحداث. قد يقوم Listener بإرسال بريد إلكتروني، أو إنشاء فاتورة، أو إشعار المستودع، أو تحديث نظام التحليلات.
طبقات DDD باختصار
البنية الشائعة في DDD تفصل النظام إلى طبقات. قد تختلف أسماء المجلدات من مشروع لآخر، لكن الفكرة العامة تبقى واحدة.
Domain Layer: تحتوي على Entities وValue Objects وAggregates وDomain Services وDomain Events.
Application Layer: تحتوي على حالات الاستخدام، Commands، Handlers، وApplication Services.
Infrastructure Layer: تحتوي على تنفيذ قواعد البيانات، APIs الخارجية، Queues، Mail، Storage، وتفاصيل الإطار البرمجي.
Interface Layer: تحتوي على Controllers وAPI Resources وRequests وCLI Commands وAdapters الخاصة بالواجهة.
القاعدة الأهم هي اتجاه الاعتماد. لا يجب أن يعتمد المجال على Laravel أو Eloquent أو HTTP Requests أو Queues أو قاعدة البيانات. يجب أن يحتوي المجال على منطق العمل بشكل نقي قدر الإمكان.
مثال على بنية مجلدات DDD في Laravel
Laravel لا يفرض عليك استخدام DDD، لكن يمكنك تنظيم تطبيق Laravel بطريقة قريبة من DDD.
app/
├── Domain/
│ └── Orders/
│ ├── Entities/
│ │ ├── Order.php
│ │ └── OrderItem.php
│ ├── ValueObjects/
│ │ ├── Money.php
│ │ └── OrderStatus.php
│ ├── Events/
│ │ └── OrderWasPlaced.php
│ ├── Repositories/
│ │ └── OrderRepository.php
│ └── Services/
│ └── ShippingCostCalculator.php
│
├── Application/
│ └── Orders/
│ ├── Commands/
│ │ └── PlaceOrderCommand.php
│ └── Services/
│ └── PlaceOrderService.php
│
├── Infrastructure/
│ └── Persistence/
│ └── Eloquent/
│ ├── Models/
│ │ └── OrderModel.php
│ ├── Mappers/
│ │ └── OrderMapper.php
│ └── Repositories/
│ └── EloquentOrderRepository.php
│
└── Http/
└── Controllers/
└── OrderController.phpهذه البنية مفيدة عندما يحتوي المشروع على قواعد عمل جدية. أما في مشروع CRUD صغير جداً، فقد تكون هذه البنية أكبر من الحاجة. DDD يجب أن يعالج التعقيد، لا أن يخلق تعقيداً جديداً.
مثال Controller بأسلوب DDD
في تطبيق Laravel يستخدم DDD، يجب أن يكون الكنترولر بسيطاً. يستقبل الطلب، يتحقق من البيانات، يستدعي Application Service، ثم يعيد response.
final class OrderController
{
public function __construct(
private readonly PlaceOrderService $placeOrderService
) {
}
public function store(PlaceOrderRequest $request): JsonResponse
{
$command = new PlaceOrderCommand(
customerId: $request->input('customer_id'),
items: $request->input('items')
);
$orderId = $this->placeOrderService->handle($command);
return response()->json([
'order_id' => $orderId,
'message' => 'Order placed successfully.',
], 201);
}
}الكنترولر لا يعرف كيف يتم حساب مجموع الطلب. ولا يعرف قواعد الدفع. ولا يغيّر حالة الطلب مباشرة. هو فقط يفوّض حالة الاستخدام إلى طبقة التطبيق.
مثال Application Service
final class PlaceOrderService
{
public function __construct(
private readonly OrderRepository $orders,
private readonly ProductCatalog $products
) {
}
public function handle(PlaceOrderCommand $command): string
{
$order = Order::createForCustomer($command->customerId);
foreach ($command->items as $item) {
$product = $this->products->findById($item['product_id']);
if ($product === null) {
throw new RuntimeException('Product not found.');
}
$order->addItem(
productId: $product->id(),
quantity: $item['quantity'],
price: $product->price()
);
}
$order->place();
$this->orders->save($order);
return $order->id();
}
}Application Service ينسّق حالة الاستخدام. لكن كائن Order يبقى مسؤولاً عن قواعد العمل المهمة: هل يمكن إضافة عنصر؟ هل يمكن تأكيد الطلب؟ وكيف يتم حساب المجموع؟
DDD مقابل CRUD التقليدي
CRUD يركز على إنشاء السجلات وقراءتها وتحديثها وحذفها. هذا مناسب للوحات الإدارة البسيطة، والمدونات، والأنظمة الصغيرة التي لا تحتوي على قواعد عمل معقدة.
أما DDD فهو مناسب عندما يحتوي النظام على سلوك، وقواعد، وسير عمل، وقرارات تجارية. في DDD لا يكون السؤال فقط: “كيف نحدّث هذا الصف في قاعدة البيانات؟” السؤال الأفضل هو: “ما الفعل التجاري الذي يحدث؟ وما القواعد التي يجب حمايتها؟”
| تفكير CRUD | تفكير DDD |
|---|---|
| تحديث حالة الطلب | إلغاء الطلب، شحن الطلب، دفع الطلب |
| حفظ سجل الدفع | تحصيل الدفع أو استرداده |
| تعديل الاشتراك | تجديد، إيقاف، ترقية، أو إلغاء الاشتراك |
| تغيير حقل في الفاتورة | إصدار الفاتورة، تعليمها كمدفوعة، أو إلغاؤها |
DDD يدفعك إلى تمثيل أفعال العمل بوضوح داخل الكود.
متى يجب استخدام DDD؟
DDD مفيد عندما تكون قواعد العمل معقدة لدرجة أن CRUD البسيط لم يعد كافياً.
فكّر في استخدام DDD عندما:
يحتوي النظام على عدد كبير من قواعد العمل.
تستخدم الفرق المختلفة نفس المصطلحات بمعانٍ مختلفة.
يتكرر منطق العمل بين Controllers وJobs وServices.
تعديل ميزة واحدة يكسر ميزة أخرى كثيراً.
يحتوي التطبيق على سير عمل مثل الطلبات، المدفوعات، الموافقات، الفواتير، الاشتراكات، أو الشحن.
تحتاج إلى اختبارات قوية حول سلوك العمل.
المشروع سيستمر ويتوسع لسنوات.
DDD ليس ضرورياً لكل مشروع. إذا كنت تبني مدونة بسيطة، أو صفحة تعريفية، أو لوحة CRUD صغيرة، فقد تكون بنية Laravel التقليدية كافية تماماً.
متى يصبح DDD مبالغاً فيه؟
قد يصبح DDD ضاراً عندما يُطبّق بشكل ميكانيكي بدون وجود تعقيد حقيقي في العمل. إنشاء طبقات كثيرة، وواجهات كثيرة، وFactories، وCommands، وHandlers، وMappers لكل عملية بسيطة قد يبطئ التطوير إذا لم يكن المشروع بحاجة إلى ذلك.
DDD السيئ غالباً يبدو هكذا:
كل Model بسيط لديه Interface بدون سبب واضح.
كل عملية صغيرة تحتاج عدداً كبيراً من الكلاسات.
طبقة Domain لا تحتوي على منطق عمل حقيقي.
بنية المجلدات تبدو متقدمة، لكن الكود ما زال CRUD فقط.
المطورون يقضون وقتاً أطول في التنقل بين الملفات بدلاً من حل المشكلة.
DDD يجب أن يقلل غموض العمل. إذا كان يضيف تعقيداً تقنياً فقط، فهو يُستخدم بطريقة خاطئة.
أخطاء شائعة في DDD
الخطأ الأول هو التعامل مع DDD كبنية مجلدات فقط. نقل الملفات إلى مجلدات باسم Domain وApplication لا يعني أن لديك تصميماً جيداً. الأهم هو أين تعيش قواعد العمل، وهل يعبّر الكود عن لغة العمل بوضوح.
الخطأ الثاني هو جعل الـ Entities فارغة جداً. إذا كان الـ Entity يحتوي فقط على getters وsetters، فهو لا يحمي أي سلوك حقيقي.
// Weak model
$order->setStatus('cancelled');
// Better model
$order->cancel();النسخة الثانية أفضل لأنها تعطي الطلب فرصة للتحقق: هل الإلغاء مسموح أصلاً؟
الخطأ الثالث هو وضع كل المنطق داخل Services. هذا يؤدي إلى ما يسمى Anemic Domain Model، حيث تصبح الـ Entities مجرد حاويات بيانات، بينما تتحول الخدمات إلى سكربتات ضخمة.
الخطأ الرابع هو خلط البنية التحتية مع منطق المجال. مثلاً، إذا كان الـ Entity يرسل إيميلات، أو يكتب logs، أو يستخدم Laravel Facades، أو يستدعي API خارجي مباشرة، فهذا يعني أن المجال أصبح مرتبطاً بتفاصيل تقنية.
DDD والاختبارات
واحدة من أقوى فوائد DDD هي سهولة اختبار قواعد العمل. لأن منطق المجال منفصل عن الكنترولرات وقواعد البيانات، يمكنك اختبار السلوك مباشرة.
public function test_shipped_order_cannot_be_cancelled(): void
{
$order = Order::createForCustomer('customer-1');
$order->addItem('product-1', 1, new Money(1000, 'USD'));
$order->place();
$order->markAsShipped();
$this->expectException(DomainException::class);
$order->cancel();
}هذا الاختبار لا يحتاج HTTP، ولا قاعدة بيانات، ولا Migrations، ولا Middleware، ولا API Request. هو يختبر قاعدة العمل مباشرة.
DDD في المشاريع الحقيقية
في المشاريع الحقيقية، غالباً يتم إدخال DDD تدريجياً. لا تحتاج إلى إعادة كتابة النظام بالكامل. الطريقة العملية هي البدء بأكثر جزء يحتوي على تعقيد حقيقي في العمل.
مثلاً، في نظام تجارة إلكترونية، قد يبقى قسم المنتجات Catalog بسيطاً بأسلوب CRUD، بينما يتم تطبيق DDD في الطلبات والمدفوعات لأنها تحتوي على قواعد أكثر حساسية.
مسار عملي لتطبيق DDD قد يكون كالتالي:
حدد أكثر Workflow معقد في النظام.
اكتب لغة العمل الحقيقية التي يستخدمها الفريق.
استخرج Entities وValue Objects وAggregates وDomain Events.
انقل قواعد العمل خارج الكنترولرات.
أنشئ Application Services لحالات الاستخدام المهمة.
استخدم Repositories فقط عندما يكون فصل التخزين مفيداً.
أضف اختبارات لسلوك المجال.
قم بالتحسين تدريجياً ولا تعيد كتابة كل شيء مرة واحدة.
DDD وMicroservices
غالباً يتم ذكر DDD مع Microservices، لكنهما ليسا نفس الشيء. DDD يساعدك على فهم حدود العمل. أما Microservices فهي أسلوب معماري وتشغيلي لتوزيع النظام.
قد يتحول Bounded Context إلى Microservice، لكنه ليس مضطراً لذلك. في كثير من الحالات، من الأفضل البدء بـ Modular Monolith. تفصل السياقات داخل نفس الكودباس أولاً. لاحقاً، إذا ظهرت حاجة حقيقية، يمكن استخراج بعض السياقات كخدمات مستقلة.
نظام Modular Monolith واضح الحدود غالباً أفضل من نظام موزع بخدمات كثيرة لكن بحدود عمل غير واضحة.
Anti-Corruption Layer
Anti-Corruption Layer هي طبقة تحمي نموذج المجال من الأنظمة الخارجية أو الكود القديم. وظيفتها ترجمة البيانات الخارجية إلى مفاهيم يفهمها المجال.
مثلاً، مزود الدفع قد يعيد حالات مثل:
AUTH_OKCAPTURED_FULLREVERSAL_DONE
بينما يستخدم المجال مصطلحات أوضح:
authorizedcapturedrefunded
Anti-Corruption Layer تمنع تسرب لغة الأنظمة الخارجية إلى نموذج العمل الداخلي.
final class PaymentStatusTranslator
{
public function translate(string $providerStatus): PaymentStatus
{
return match ($providerStatus) {
'AUTH_OK' => PaymentStatus::authorized(),
'CAPTURED_FULL' => PaymentStatus::captured(),
'REVERSAL_DONE' => PaymentStatus::refunded(),
default => throw new DomainException('Unknown payment status.'),
};
}
}Checklist عملي لتطبيق DDD
قبل تطبيق DDD، اسأل هذه الأسئلة:
ما المشكلة الأساسية في مجال العمل؟
ما المصطلحات التي يستخدمها أصحاب العمل أو فريق المنتج؟
ما المفاهيم التي تمتلك هوية مستقلة؟
ما المفاهيم التي يمكن تمثيلها كـ Value Objects؟
ما الكائنات التي يجب أن تبقى متسقة معاً؟
أين يجب أن تعيش كل قاعدة عمل؟
ما الأفعال التي تستحق أن تكون دوال واضحة داخل النموذج؟
ما الأحداث المهمة في العمل؟
ما الذي يعتبر Domain Logic وما الذي يعتبر Infrastructure؟
هل يمكن اختبار قواعد العمل بدون HTTP وبدون قاعدة بيانات؟
قاعدة بسيطة لتذكر DDD
إذا كان الكود يقول ما يقصده العمل، فأنت تسير في الاتجاه الصحيح.
بدلاً من هذا:
$order->status = 'cancelled';
$order->save();استخدم هذا:
$order->cancel();
$orderRepository->save($order);النسخة الثانية تعطي نموذج المجال فرصة لحماية قواعده. وهذا هو جوهر DDD.
الخلاصة
Domain-Driven Design لا يهدف إلى جعل البرمجيات تبدو أكثر تعقيداً. هدفه الحقيقي هو جعل تعقيد العمل واضحاً، منظماً، وقابلاً للحماية. عندما يُستخدم بشكل صحيح، يساعد DDD المطورين على بناء أنظمة تتطابق مع لغة العمل وقواعده وسير عملياته.
الدرس الأهم هو أن منطق العمل لا يجب أن يكون مخفياً داخل الكنترولرات، أو تحديثات قاعدة البيانات، أو خدمات ضخمة مبعثرة. يجب أن يعيش داخل نماذج مجال واضحة تعبّر عن ما يفعله النظام فعلياً.
DDD قوي بشكل خاص في التطبيقات التي تحتوي على طلبات، مدفوعات، اشتراكات، فواتير، موافقات، شحن، سير عمل مؤسسي، أو أي مجال تكون فيه القواعد أهم من مجرد تخزين البيانات. استخدمه بحذر، وابدأ من المناطق الأكثر تعقيداً، ودع نموذج العمل يقود بنية الكود.

