لماذا يقتل CRUD العادي مشروعك، وكيف يمكن لـ DDD إصلاح ذلك
\n\nكل مطور واجه مرة واحدة على الأقل موقفاً يتحول فيه المشروع البسيط ظاهرياً إلى "مستنقع" من الخدمات (services)، وحدات التحكم (controllers)، ومنطق الأعمال المبعثر في جميع أنحاء الكود. تبدأ بكتابة UserService، ثم OrderService، وبعد ستة أشهر تجد أن OrderService يحتوي على منطق إرسال البريد الإلكتروني، وحساب الخصومات، والتحقق من رصيد المستخدم. هل هذا مألوف؟
المشكلة هي أننا نفكر مبكراً جداً في قواعد البيانات، والأطر (frameworks)، وواجهات برمجة التطبيقات REST API. ننسى الشيء الأهم - وهو المجال (domain). يقدم التصميم المدفوع بالمجال (Domain-Driven Design - DDD) نهجاً مختلفاً جذرياً: أولاً نقوم بنمذجة عمليات الأعمال بلغة الخبراء، ثم بعد ذلك - الكود.
\n\nفي هذه المقالة، سنشرح كيفية تطبيق التصميم المدفوع بالمجال في TypeScript مع أمثلة حقيقية. سترى كيفية بناء هندسة نظيفة حيث تعيش قواعد الأعمال منفصلة عن البنية التحتية (infrastructure)، ويكون الكود سهلاً للاختبار والتعديل.
\n\nأساسيات DDD: اللغة، السياقات والنموذج
\n\nاللغة الموحدة (Ubiquitous Language)
\nالمبدأ الأول لـ DDD هو لغة موحدة، مفهومة لكل من المطورين وخبراء الأعمال. إذا قال المدير "قام العميل بتفعيل الاشتراك"، فيجب أن يكون هناك في الكود طريقة activateSubscription()، وليس setStatus(1). TypeScript مع كتابته الصارمة (strict typing) مثالي لهذا - تصبح أسماء الفئات (classes)، والطرق (methods)، والمتغيرات (variables) توثيقاً.
السياق المحدود (Bounded Context)
\nينقسم النظام الكبير إلى سياقات مستقلة: "المدفوعات"، "إدارة المستخدمين"، "التوصيل". في كل سياق يوجد نموذجه ولغته الخاصة. تساعد وحدات TypeScript والأنواع الصارمة (strict types) في فصل هذه السياقات بشكل صارم على مستوى الاستيراد (imports).
\n\nبناء نموذج المجال على TypeScript
\n\nكائن القيمة (Value Object)
\nهو كيان غير قابل للتغيير (immutable) يتم تعريفه من خلال سماته (attributes)، وليس بواسطة معرف (identifier). مثال: Email، Money، Coordinates.
\n\n// value-objects/Email.ts\nexport class Email {\n private readonly value: string;\n\n constructor(email: string) {\n if (!this.isValid(email)) {\n throw new Error('Invalid email address');\n }\n this.value = email;\n }\n\n private isValid(email: string): boolean {\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);\n }\n\n public getValue(): string {\n return this.value;\n }\n\n public equals(other: Email): boolean {\n return this.value === other.value;\n }\n}\n\n\nلاحظ: Email يتحقق بنفسه من صحته. لا ينبغي لأي خدمة (service) أن تعرف كيف يبدو البريد الإلكتروني الصحيح. هذا هو التصميم المدفوع بالمجال في TypeScript الخالص - قاعدة الأعمال تعيش داخل الكائن.
الكيان (Entity)
\nعلى عكس كائن القيمة، الكيان له معرف (ID) ويمكن أن يتغير بمرور الوقت. مثال: User، Order.
\n\n// entities/User.ts\nimport { Email } from '../value-objects/Email';\nimport { UserId } from '../value-objects/UserId';\n\nexport class User {\n private readonly id: UserId;\n private email: Email;\n private isActive: boolean;\n\n constructor(id: UserId, email: Email) {\n this.id = id;\n this.email = email;\n this.isActive = true;\n }\n\n public getId(): UserId {\n return this.id;\n }\n\n public changeEmail(newEmail: Email): void {\n if (!this.isActive) {\n throw new Error('Cannot change email for inactive user');\n }\n this.email = newEmail;\n }\n\n public deactivate(): void {\n this.isActive = false;\n }\n}\n\n\nالتجميع (Aggregate)
\nالتجميع هو مجموعة من الكيانات وكائنات القيمة التي يتم معالجتها كوحدة واحدة. للتجميع جذر (Aggregate Root) يتم من خلاله الوصول إلى الكائنات الداخلية.
\n\nمثال: الطلب (Order)