> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# التصميم الموجه بالمجال Domain-Driven Design ببساطة
- URL: https://arabroot.io/articles/التصميم-الموجه-بالمجال-ddd-ببساطة/
- Published: 2026-10-07T13:00:00.000Z
- Updated: 2026-10-09T13:02:56.000Z
- Description: لماذا تعني كلمة «الطلب» ثلاثة أشياء في نفس الشركة، وكيف يقسم DDD النظام إلى سياقات لكل منها لغته ونموذجه.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, هندسة البرمجيات وتصميم الأنظمة, DevOps وCI/CD, الاستضافة الذاتية

لنفرض أن لديك متجراً إلكترونياً صغيراً بدأ بجدول واحد اسمه `Orders` وكلاس واحد اسمه `Order`، ومع الوقت أضاف فريق المبيعات حقول الخصم والكوبون، وأضاف فريق الشحن حقول العنوان والوزن ورقم التتبع، وأضاف فريق الفوترة حقول الضريبة وحالة الدفع، وبعد سنتين أصبح الكلاس فيه ستون حقلاً وعشرون حالة، وكل تعديل يطلبه فريق الشحن يكسر شيئاً عند فريق الفوترة، وفي كل اجتماع يقول أحدهم «الطلب» ويقصد شيئاً مختلفاً عما يقصده الآخر. وهذه المشكلة ليست في لغة البرمجة ولا في قاعدة البيانات، وإنما في طريقة فهمنا للمجال الذي نبني له البرنامج وطريقة تقسيمه، وهذا بالضبط ما يعالجه التصميم الموجه بالمجال Domain-Driven Design واختصاراً DDD.

وفي [مدخل هذه المرحلة](https://arabroot.io/articles/%D8%A7%D9%84%D9%81%D8%B1%D9%82-%D8%A8%D9%8A%D9%86-%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A7%D8%AA-%D9%88%D9%85%D8%B9%D9%85%D8%A7%D8%B1%D9%8A%D8%AA%D9%87%D8%A7-%D9%88%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9/) شرحنا الفرق بين تصميم البرمجيات Software Design ومعمارية البرمجيات Software Architecture، وهنا سوف نأخذ أداة تقع بين الاثنين، فهي تبدأ من الحديث مع أهل العمل وتنتهي بأسماء الكلاسات والدوال. وسوف نناقش في هذا المقال ما يلي:

- ما هو DDD، ولماذا قسمه الناس إلى جانب استراتيجي Strategic وجانب تكتيكي Tactical.
- المجالات الفرعية Subdomains: أين تضع أفضل مهندسيك، وأين تستخدم أداة جاهزة.
- اللغة المشتركة Ubiquitous Language، والسياق المحدد Bounded Context، وخريطة السياقات Context Map وعلاقاتها.
- ورشة EventStorming باختصار.
- الكيان Entity وكائن القيمة Value Object وال Aggregate والحدث Domain Event، بكود C# حقيقي ومخرجاته، مع نفس كائن القيمة في Python.
- متى يكون DDD أكبر من حاجتك، والأخطاء الشائعة عند تطبيقه.

## ما هو التصميم الموجه بالمجال Domain-Driven Design؟

المجال Domain هو مجال المعرفة أو النشاط الذي يخدمه البرنامج، فمجال برنامج المتجر هو البيع والشحن والفوترة، ومجال برنامج العيادة هو المواعيد والملفات الطبية، والنموذج Model هو مجموعة المفاهيم المختارة من هذا المجال التي نستخدمها لحل مشكلاته، وهذان التعريفان مأخوذان من وثيقة [DDD Reference](https://www.domainlanguage.com/ddd/reference/?ref=arabroot.io) التي نشرها Eric Evans عام 2015 بترخيص Creative Commons، وهي ملخص رسمي لأنماط كتابه الصادر عام 2003\. ويلخص Evans في هذه الوثيقة الفكرة كلها في ثلاث نقاط: أن تركز على المجال الأساسي Core Domain، وأن تبني النموذج بالتعاون بين أهل المجال والمطورين، وأن تتكلم لغة مشتركة داخل سياق محدد بوضوح.

ويصف Martin Fowler في مقاله [DomainDrivenDesign](https://martinfowler.com/bliki/DomainDrivenDesign.html?ref=arabroot.io) هذا الأسلوب بأنه يجعل التطوير يدور حول نموذج للمجال يفهم عملياته وقواعده، ويقول إنه مناسب بالذات للمجالات المعقدة التي فيها منطق كثير متشابك، وهذه الجملة الأخيرة مهمة جداً لأننا سوف نعود إليها في آخر المقال عندما نتكلم عن الحالات التي لا تحتاج فيها DDD أصلاً.

والناس يقسمون DDD إلى جزأين:

- **الجانب الاستراتيجي Strategic Design**: كيف تقسم المجال الكبير إلى أجزاء، وما اللغة في كل جزء، وكيف تتعامل الأجزاء والفرق مع بعضها. وهو لا يحتاج سطراً واحداً من الكود، ويفيد أي فريق تقريباً.
- **الجانب التكتيكي Tactical Design**: أنماط داخل الكود مثل الكيان Entity وكائن القيمة Value Object وال Aggregate والحدث Domain Event وال Repository، وهي تفيد عندما تكون القواعد كثيرة ومعقدة، ولها ثمن في الوقت والتعقيد.

وقد يتساءل البعض: هل DDD إطار عمل Framework أو طريقة لترتيب المجلدات؟ والإجابة لا، فهو طريقة تفكير وأسماء لأنماط، ولا يوجد شيء تقوم بتثبيته، وإذا رأيت مشروعاً فيه مجلدات اسمها `Domain` و`Aggregates` لكن أسماء الكلاسات فيه لا يفهمها أحد من أهل العمل، فهو لم يطبق DDD وإنما استعار أسماءه.

## المجالات الفرعية Subdomains: أين تضع أفضل جهدك؟

المتجر ليس مجالاً واحداً، فيه البيع والتسعير، والشحن، والفوترة، وحسابات المستخدمين، وكل جزء من هذه مجال فرعي Subdomain، وليست كلها بنفس الأهمية. ويقسمها دليل Microsoft في صفحة [Domain analysis for microservices](https://learn.microsoft.com/en-us/azure/architecture/microservices/model/domain-analysis?ref=arabroot.io) إلى ثلاثة أنواع:

- **المجال الأساسي Core Domain**: الجزء الذي يميز الشركة عن منافسيها ويعطيها الميزة، وهو يستحق النمذجة الدقيقة وأفضل المهندسين. ويقول Evans في نمط Core Domain إن هذا الجزء يجب أن يكون صغيراً وأن تضع فيه أفضل الكفاءات، لأن المهندسين الأقوياء يميلون عادة إلى البنية التقنية بدل فهم المجال.
- **المجال الداعم Supporting Subdomain**: جزء يحتاجه العمل ويخص الشركة فلا تجده جاهزاً كما هو، لكنه ليس سبب تميزها، مثل الفوترة في كثير من المتاجر.
- **المجال العام Generic Subdomain**: مشكلة حلها العالم من قبل، مثل تسجيل الدخول وإدارة المستخدمين، والأنسب أن تستخدم فيها حلاً جاهزاً، ويقول Evans في نمط Generic Subdomains إن عليك أن تفكر في الحلول الجاهزة لهذه الأجزاء ولا تضع فيها مهندسي المجال الأساسي.

ولنفرض أن متجرنا ينافس بالعروض والتسعير الذكي، فالمبيعات والتسعير هي المجال الأساسي، والشحن والفوترة مجالات داعمة، والهوية والدخول مجال عام لا يستحق أن تكتبه بنفسك، فتستخدم مثلاً [Authentik للدخول الموحد](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-authentik-%D9%84%D9%84%D8%AF%D8%AE%D9%88%D9%84-%D8%A7%D9%84%D9%85%D9%88%D8%AD%D8%AF/) وتنتهي منه. ولاحظ أن التصنيف يختلف من شركة إلى أخرى، فالشحن عند شركة توصيل هو المجال الأساسي نفسه.

## اللغة المشتركة Ubiquitous Language

اللغة المشتركة Ubiquitous Language هي لغة يتفق عليها الفريق كله، المطورون وخبراء المجال Domain Experts، ويستخدمونها في الكلام والمستندات والكود والاختبارات، داخل سياق محدد واحد، وهذا تعريف Evans في الوثيقة المرجعية. ويضيف Fowler في مقاله [UbiquitousLanguage](https://martinfowler.com/bliki/UbiquitousLanguage.html?ref=arabroot.io) أن هذه اللغة يجب أن تكون دقيقة لأن البرنامج لا يتحمل الغموض، وأنها تتطور كلما زاد فهم الفريق للمجال.

الصورة التالية تبين الفكرة: جملة يقولها أهل العمل، ثم قاموس صغير يتفق عليه الفريق، ثم الكود الذي يستخدم نفس الكلمات بدل أسماء لا يفهمها إلا من كتبها:

![ثلاثة أعمدة: في اليمين خبراء المجال يقولون لا نشحن أي طلب قبل أن يتأكد الدفع، وفي الوسط قاموس مشترك فيه Order وPlace وPaymentConfirmed وShipment وShip مع معانيها بالعربية، وفي اليسار كود بأسماء غامضة مثل UpdateStatus(o, 4) باللون الأحمر، وكود بكلمات العمل مثل order.ConfirmPayment وorder.Ship باللون الأخضر](https://arabroot.io/content/images/2026/10/ddd-basics-02-language.webp)

الكلمات التي يقولها أهل العمل هي نفسها أسماء الكلاسات والدوال

الطريقة البسيطة التي يبدأ بها أغلب الفرق أن يسمع المطور المتطلبات بلغة أهل العمل ثم يكتب الكود بأسمائه هو: `UpdateStatus(o, 4)` و`Flag2` و`ProcessItem`، وتعمل هذه الطريقة في البداية، لكنها تفشل لسببين: الأول أن كل حديث مع أهل العمل يحتاج ترجمة في رأس المطور من «تأكيد الدفع» إلى «الحالة 4»، وكل ترجمة فرصة لخطأ، والثاني أن المطور الجديد يقرأ الكود فلا يفهم القاعدة، ويقرأ وثيقة المتطلبات فلا يجد مكانها في الكود. أما الطريقة الصحيحة فهي أن تكون القاعدة «لا نشحن قبل تأكيد الدفع» موجودة في الكود بنفس الكلمات تقريباً: دالة اسمها `Ship` ترفض العمل إذا لم يحدث `PaymentConfirmed`.

وفي الفرق العربية يتكلم أهل العمل بالعربية ويكتب المطورون الكود بالإنجليزية، وهنا ننصح بقاموس صغير في المستودع Repository، مثلاً ملف `docs/glossary.md`، فيه المصطلح العربي كما يقوله أهل العمل، والاسم الإنجليزي الذي يستخدمه الكود، وتعريف من سطر واحد، وأي كلمة جديدة تظهر في اجتماع تضاف إليه قبل أن تظهر في الكود. وإذا قال لك خبير المجال إن كلمة في الكود لا تعني له شيئاً، فغالباً الاسم خطأ وليس الخبير.

## السياق المحدد Bounded Context: كلمة واحدة لها أكثر من معنى

عندما تبدأ بكتابة القاموس سوف تكتشف بسرعة أن نفس الكلمة لها معان مختلفة في أقسام الشركة، والصورة التالية تبين ذلك في متجرنا:

![كلمتا Order وCustomer في الأعلى، وتحتهما ثلاث بطاقات: SALES CONTEXT بعربة تسوق وحقول OrderId وLines وDiscount وStatus، وSHIPPING CONTEXT بشاحنة وحقول Recipient وAddress وParcels وTrackingNumber، وBILLING CONTEXT بفاتورة وحقول Amount وVAT وPaymentStatus، وتحت كل بطاقة معنى العميل في ذلك القسم](https://arabroot.io/content/images/2026/10/ddd-basics-01-one-word.webp)

«الطلب» عند المبيعات غير «الطلب» عند الشحن والفوترة، و«العميل» كذلك

الطريقة البسيطة هي أن تبني نموذجاً واحداً موحداً للشركة كلها: كلاس `Order` واحد وجدول واحد فيه كل الحقول التي يحتاجها الجميع، وهذا ما كان ينصح به الناس قديماً كما يذكر Fowler في مقاله [BoundedContext](https://martinfowler.com/bliki/BoundedContext.html?ref=arabroot.io)، حيث يحكي أنه عمل في شركة كهرباء كانت كلمة «العداد» فيها تعني أشياء مختلفة بحسب القسم، ثم ينقل عن Evans أن توحيد نموذج المجال بالكامل في نظام كبير غير ممكن وغير مجد اقتصادياً. والسبب أن النموذج الموحد يجبر كل قسم على حمل حقول وقواعد أقسام أخرى، فيكبر الكلاس، وتتعارض القواعد، ويصبح كل تغيير اجتماعاً بين ثلاثة فرق.

والحل الذي يقترحه DDD هو السياق المحدد Bounded Context، وهو حد واضح يكون داخله نموذج واحد متسق ولغة واحدة، وخارجه قد تعني نفس الكلمة شيئاً آخر. ويطلب Evans في الوثيقة المرجعية أن ترسم هذا الحد صراحة بثلاث طرق: من يعمل عليه من الفرق، وأي جزء من التطبيق يستخدمه، وأين يظهر فعلياً مثل المستودع Code Base ومخطط قاعدة البيانات Schema. ففي متجرنا سياق المبيعات فيه `Order` بسطور وأسعار، وسياق الشحن فيه `Shipment` بعنوان ووزن، وسياق الفوترة فيه `Invoice` بمبلغ وضريبة، وكلها تشير إلى نفس رقم الطلب `OrderId` لكن لا يحمل أي منها نموذج الآخر.

وقد يتساءل البعض: هل كل Bounded Context خدمة مصغرة Microservice؟ والإجابة لا بالضرورة، فتقول Microsoft في صفحة [Identify domain-model boundaries](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/architect-microservice-container-applications/identify-microservice-domain-model-boundaries?ref=arabroot.io) إن تصميم الخدمات المصغرة يبدأ من حدود السياقات، وإن السياق الواحد قد يتكون من عدة خدمات فعلية، لكن العكس غير صحيح، أي لا ينبغي أن تقسم خدمة واحدة على عدة سياقات. ويمكن أيضاً أن يكون كل سياق وحدة Module داخل تطبيق واحد، وهذا ما نشرحه في [الـ Modular Monolith](https://arabroot.io/articles/%D8%A7%D9%86%D9%85%D8%A7%D8%B7-%D8%A7%D9%84%D9%85%D8%B9%D9%85%D8%A7%D8%B1%D9%8A%D8%A9-layered-%D9%88-event-driven-%D9%88-modular-monolith/#modular-monolith)، وهو غالباً البداية الأنسب للفرق الصغيرة قبل أن تفكر في [الخدمات المصغرة](https://arabroot.io/articles/%D8%A7%D9%86%D9%85%D8%A7%D8%B7-%D8%A7%D9%84%D9%85%D8%B9%D9%85%D8%A7%D8%B1%D9%8A%D8%A9-layered-%D9%88-event-driven-%D9%88-modular-monolith/#microservices).

💡

العلامة العملية على أنك تحتاج حداً جديداً: عندما يحتاج تعريف كلمة في القاموس إلى «يعتمد على القسم»، أو عندما تجد في الكلاس حقولاً لا يستخدمها إلا فريق واحد. وتذكر أن الحد بين السياقات حد لغة ونموذج قبل أن يكون حد شبكة.

## خريطة السياقات Context Map: كيف تتعامل السياقات مع بعضها؟

بعد تقسيم النظام إلى سياقات تبقى مسألة مهمة، لأن السياقات تتبادل البيانات، والفرق التي تملكها لها علاقات قوة ومصالح مختلفة، فهناك فريق يقرر شكل البيانات ويسمى المصدر Upstream، وفريق يستهلكها ويتأثر بقراراته ويسمى المستهلك Downstream. وخريطة السياقات Context Map رسم يبين كل سياق في المشروع والأنظمة التي تتكامل معه، ونوع العلاقة بين كل اثنين، ويطلب Evans أن تكون أسماء السياقات جزءاً من اللغة المشتركة. والصورة التالية خريطة لمتجرنا:

![خريطة سياقات: Sales بلون برتقالي مكتوب عليه CORE، وShipping وBilling بلونين أزرق وأخضر مكتوب عليهما SUPPORTING، وIdentity رمادي مكتوب عليه GENERIC، وPayment أحمر مكتوب عليه EXTERNAL. خط من Sales إلى Shipping عليه OHS + PL: OrderPlaced v1، وخط من Sales إلى Billing عليه SHARED KERNEL: Money، وخط من Identity إلى Sales عليه CONFORMIST، وبين Payment وBilling درع مكتوب تحته ACL، مع حرفي U وD عند طرفي كل علاقة، ومربع يشرح الألوان](https://arabroot.io/content/images/2026/10/ddd-basics-03-context-map.webp)

كل خط في الخريطة علاقة بين فريقين ونموذجين، والحرفان U وD يبينان من يقرر ومن يتأثر

وأنواع العلاقات كما تسردها الوثيقة المرجعية، مع ما يقابلها في متجرنا:

| العلاقة                                                   | معناها                                                                                    | متى تختارها                                                                                                                                                                                                      |
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| الشراكة Partnership                                       | فريقان ينجحان معاً أو يفشلان معاً، فيخططان معاً ويطوران الواجهة بينهما معاً               | ميزة واحدة تحتاج تغييراً في السياقين لنفس الإصدار                                                                                                                                                                |
| النواة المشتركة Shared Kernel                             | جزء صغير من النموذج والكود يتشاركه فريقان، ولا يغيره أحدهما دون الآخر                     | كائن مثل Money بين المبيعات والفوترة، بشرط أن يبقى صغيراً جداً                                                                                                                                                   |
| العميل والمورد Customer/Supplier                          | المصدر يضع احتياجات المستهلك في خطته، ويتفقان على اختبارات قبول للواجهة                   | الشحن يحتاج حقلاً جديداً في حدث المبيعات، والمبيعات تلتزم به                                                                                                                                                     |
| التبعية Conformist                                        | المستهلك يقبل نموذج المصدر كما هو لأن المصدر لن يغيره لأجله                               | المبيعات تستخدم بيانات المستخدم كما يرسلها نظام الهوية عبر [OpenID Connect](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-oauth-%D9%88-openid-connect-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/) |
| طبقة الحماية من الفساد Anticorruption Layer واختصاراً ACL | المستهلك يبني طبقة تترجم نموذج النظام الآخر إلى لغته، فلا يتسرب النموذج الغريب إلى نموذجه | بوابة الدفع الخارجية أو نظام قديم Legacy بأسماء وحالات غريبة                                                                                                                                                     |
| الخدمة المفتوحة Open Host Service واختصاراً OHS           | المصدر يقدم واجهة عامة موثقة للجميع بدل مترجم خاص لكل مستهلك                              | المبيعات تنشر حدث OrderPlaced لكل من يحتاجه                                                                                                                                                                      |
| اللغة المنشورة Published Language                         | صيغة تبادل موثقة ومستقلة عن النموذج الداخلي، وتأتي غالباً مع OHS                          | مخطط JSON لحدث OrderPlaced بإصدار v1                                                                                                                                                                             |
| طرق منفصلة Separate Ways                                  | لا يوجد تكامل أصلاً، لأن ثمن التكامل أكبر من فائدته                                       | نظام إجازات الموظفين لا يحتاج أن يعرف شيئاً عن الطلبات                                                                                                                                                           |
| الكتلة الطينية Big Ball of Mud                            | جزء قديم بلا حدود واضحة، فترسم حوله حداً ولا تحاول تطبيق نمذجة دقيقة داخله                | النظام القديم الذي سوف يستبدل بعد سنة                                                                                                                                                                            |

في الجدول أعلاه لاحظ التالي:

- طبقة ACL هي أهم علاقة عملياً، لأن أغلب الأنظمة تتكامل مع أنظمة لا تملكها، وفي الكود تكون غالباً مجموعة Adapters تحول رد بوابة الدفع إلى كائنات بلغتك، وهي نفس فكرة نمط Adapter الذي شرحناه في [أنماط التصميم التي تقابلها فعلاً](https://arabroot.io/articles/%D8%A7%D9%86%D9%85%D8%A7%D8%B7-%D8%A7%D9%84%D8%AA%D8%B5%D9%85%D9%8A%D9%85-design-patterns-%D8%A7%D9%84%D8%AA%D9%8A-%D8%AA%D9%82%D8%A7%D8%A8%D9%84%D9%87%D8%A7-%D9%81%D8%B9%D9%84%D8%A7/).
- النواة المشتركة Shared Kernel تبدأ صغيرة ثم تكبر إذا لم ينتبه أحد، فإذا وجدت أن مكتبة `Common` المشتركة بين الفرق أصبح فيها كلاس `Order` كامل، فقد عدت إلى النموذج الموحد من باب خلفي.
- الخريطة تصف الواقع وليس ما تتمناه، فإذا كان فريقك فعلياً Conformist لأن الفريق الآخر لا يستجيب، فاكتب ذلك، لأن الخريطة تساعدك على التخطيط، والخريطة الكاذبة لا تساعد أحداً.

ولمن يريد قوالب جاهزة لرسم هذه الخرائط، يحتفظ مجتمع DDD Crew بمستودع مفتوح اسمه [context-mapping](https://github.com/ddd-crew/context-mapping?ref=arabroot.io) فيه شرح مختصر لكل علاقة ورموزها. أما كيف تحفظ هذه الخريطة مع بقية مخططات النظام وقراراته، فهذا موضوع [مخططات C4](https://arabroot.io/articles/%D8%AA%D9%88%D8%AB%D9%8A%D9%82-%D8%A7%D9%84%D9%85%D8%B9%D9%85%D8%A7%D8%B1%D9%8A%D8%A9-%D9%85%D8%AE%D8%B7%D8%B7%D8%A7%D8%AA-c4-%D9%88-adr/#c4) و[سجلات القرارات ADR](https://arabroot.io/articles/%D8%AA%D9%88%D8%AB%D9%8A%D9%82-%D8%A7%D9%84%D9%85%D8%B9%D9%85%D8%A7%D8%B1%D9%8A%D8%A9-%D9%85%D8%AE%D8%B7%D8%B7%D8%A7%D8%AA-c4-%D9%88-adr/#adr).

## كيف تكتشف السياقات؟ ورشة EventStorming باختصار

قد يتساءل البعض: من أين أعرف أين تقع الحدود أصلاً؟ والإجابة أنها لا تأتي من المطور وحده خلف شاشته، وإنما من جلسات مع أهل العمل، وأشهر طريقة لذلك اليوم ورشة [EventStorming](https://www.eventstorming.com/?ref=arabroot.io)، وتذكر [صفحة الكتاب الرسمية](https://www.eventstorming.com/book/?ref=arabroot.io) أن Alberto Brandolini جاء بفكرتها عام 2013 كأداة لنمذجة عمليات العمل المعقدة بسرعة بأسلوب DDD. والفكرة بسيطة في شكلها: لفافة ورق طويلة على الحائط، وأوراق لاصقة ملونة، وأهل العمل والمطورون في غرفة واحدة، يكتبون الأحداث التي تقع في العمل بصيغة الماضي («تم تأكيد الطلب»، «تم الدفع»، «تم تسليم الشحنة») ويرتبونها حسب الزمن. ويذكر الموقع الرسمي أن الورشة تستخدم لاستكشاف المجالات المعقدة، ولإظهار التناقض بين وجهات النظر، ولرسم حدود المعمارية، ولتصميم برامج تعتمد على الأحداث. والفائدة العملية أن الحدود بين السياقات تظهر على الحائط في الأماكن التي تتغير فيها الكلمات أو يتغير فيها الأشخاص الذين يتكلمون، ولمن يريد التفاصيل فكتاب Brandolini موجود على [Leanpub](https://leanpub.com/introducing%5Feventstorming?ref=arabroot.io).

## من اللغة إلى الكود: الأنماط التكتيكية

إلى هنا لم نكتب سطراً من الكود، والجانب الاستراتيجي وحده يفيد أكثر الفرق، أما الأنماط التكتيكية فهي مفيدة داخل السياق الذي فيه قواعد كثيرة، وهو عندنا سياق المبيعات. وسوف نكتب ملفاً واحداً بلغة C# فيه كائن قيمة `Money`، وAggregate اسمه `Order` يحرس قواعده، وحدث `OrderPlaced`، وسياق شحن صغير يترجم الحدث إلى لغته. والملف يعمل كتطبيق من ملف واحد File-based App، وهي ميزة جاءت مع .NET 10 كما في [توثيق Microsoft](https://learn.microsoft.com/en-us/dotnet/core/sdk/file-based-apps?ref=arabroot.io)، فلا تحتاج ملف مشروع. والكود الكامل كما يلي:

```csharp
// Sales context of a small online store: Money (Value Object), Order (Aggregate Root),
// OrderLine (inside the aggregate) and OrderPlaced (Domain Event). Run: dotnet run ddd.cs
using System.Collections.ObjectModel;

// ---------- Value Object: equal when the values are equal, never changes ----------
public sealed record Money
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        if (amount < 0) throw new ArgumentException("Money cannot be negative");
        if (currency is not ("SAR" or "USD")) throw new ArgumentException($"Unknown currency {currency}");
        Amount = amount;
        Currency = currency;
    }

    public Money Add(Money other)
    {
        if (other.Currency != Currency)
            throw new InvalidOperationException($"Cannot add {other.Currency} to {Currency}");
        return new Money(Amount + other.Amount, Currency);   // a new object, the old one stays as it was
    }

    public Money Times(int quantity) => new(Amount * quantity, Currency);
    public override string ToString() => $"{Amount:0.00} {Currency}";
}

// ---------- Domain Event: something that happened, named in the past tense ----------
public sealed record OrderPlaced(Guid OrderId, Guid CustomerId, Money Total, DateTime PlacedAtUtc);

// ---------- Entity inside the aggregate: reachable only through the root ----------
public sealed class OrderLine
{
    public string Sku { get; }
    public int Quantity { get; private set; }
    public Money UnitPrice { get; }
    internal OrderLine(string sku, int quantity, Money unitPrice) { Sku = sku; Quantity = quantity; UnitPrice = unitPrice; }
    internal void Increase(int by) => Quantity += by;
    public Money LineTotal => UnitPrice.Times(Quantity);
}

public enum OrderStatus { Draft, Placed }

// ---------- Aggregate Root: the only door to change an order ----------
public sealed class Order
{
    public const int MaxQuantityPerSku = 5;          // business rule from the sales team
    private readonly List<OrderLine> _lines = new();
    private readonly List<object> _events = new();

    public Guid Id { get; } = Guid.NewGuid();         // identity: two orders with the same lines are still two orders
    public Guid CustomerId { get; }
    public OrderStatus Status { get; private set; } = OrderStatus.Draft;
    public IReadOnlyList<OrderLine> Lines => new ReadOnlyCollection<OrderLine>(_lines);
    public IReadOnlyList<object> DomainEvents => _events;

    public Order(Guid customerId) => CustomerId = customerId;

    public Money Total => _lines.Select(l => l.LineTotal).Aggregate((a, b) => a.Add(b));

    public void AddLine(string sku, int quantity, Money unitPrice)
    {
        if (Status != OrderStatus.Draft) throw new InvalidOperationException("A placed order cannot change");
        if (quantity <= 0) throw new ArgumentException("Quantity must be at least 1");
        if (_lines.Count > 0 && _lines[0].UnitPrice.Currency != unitPrice.Currency)
            throw new InvalidOperationException("All lines of one order use one currency");

        var line = _lines.FirstOrDefault(l => l.Sku == sku);
        var newQuantity = (line?.Quantity ?? 0) + quantity;
        if (newQuantity > MaxQuantityPerSku)
            throw new InvalidOperationException($"At most {MaxQuantityPerSku} of {sku} per order (asked {newQuantity})");

        if (line is null) _lines.Add(new OrderLine(sku, quantity, unitPrice));
        else line.Increase(quantity);
    }

    public void Place()
    {
        if (Status != OrderStatus.Draft) throw new InvalidOperationException("Order already placed");
        if (_lines.Count == 0) throw new InvalidOperationException("An empty order cannot be placed");
        Status = OrderStatus.Placed;
        _events.Add(new OrderPlaced(Id, CustomerId, Total, DateTime.UtcNow));
    }
}

// ---------- Shipping context: its own model, its own words ----------
public sealed record Shipment(Guid OrderId, string Recipient, int Parcels);

public static class Shipping
{
    // Shipping does not care about prices; it translates the event into its own language
    public static Shipment OnOrderPlaced(OrderPlaced e, string recipientAddress, int itemCount)
        => new(e.OrderId, recipientAddress, Parcels: (itemCount + 2) / 3);
}

public static class Program
{
    static void Try(string title, Action action)
    {
        try { action(); Console.WriteLine($"  OK      {title}"); }
        catch (Exception ex) { Console.WriteLine($"  REFUSED {title}: {ex.Message}"); }
    }

    public static void Main()
    {
        Console.WriteLine("1) Value Object");
        var a = new Money(50m, "SAR");
        var b = new Money(50m, "SAR");
        Console.WriteLine($"  a == b: {a == b}, same object: {ReferenceEquals(a, b)}");
        var c = a.Add(new Money(25m, "SAR"));
        Console.WriteLine($"  a.Add(25) = {c}, a is still {a}");
        Try("add USD to SAR", () => a.Add(new Money(10m, "USD")));

        Console.WriteLine("2) Entity identity");
        var customer = Guid.NewGuid();
        var o1 = new Order(customer); o1.AddLine("BOOK-1", 1, a);
        var o2 = new Order(customer); o2.AddLine("BOOK-1", 1, a);
        Console.WriteLine($"  same lines, same order? {o1.Id == o2.Id}");

        Console.WriteLine("3) Aggregate rules (invariants)");
        var order = new Order(customer);
        Try("add 3 x KB-9 at 120 SAR", () => order.AddLine("KB-9", 3, new Money(120m, "SAR")));
        Try("add 2 more KB-9", () => order.AddLine("KB-9", 2, new Money(120m, "SAR")));
        Try("add 1 more KB-9", () => order.AddLine("KB-9", 1, new Money(120m, "SAR")));
        Try("add a line priced in USD", () => order.AddLine("MOUSE-2", 1, new Money(20m, "USD")));
        Try("place an empty order", () => new Order(customer).Place());
        Console.WriteLine($"  total = {order.Total}, status = {order.Status}");

        Console.WriteLine("4) Domain Event");
        order.Place();
        Try("add a line after placing", () => order.AddLine("CABLE-1", 1, new Money(15m, "SAR")));
        foreach (var e in order.DomainEvents) Console.WriteLine($"  event: {e.GetType().Name} total={((OrderPlaced)e).Total}");

        var placed = (OrderPlaced)order.DomainEvents[0];
        var shipment = Shipping.OnOrderPlaced(placed, "Building 7, Example Street", order.Lines.Sum(l => l.Quantity));
        Console.WriteLine($"  shipping created: {shipment.Parcels} parcels to {shipment.Recipient}");
    }
}
```

والتشغيل بالأمر التالي:

```bash
dotnet run ddd.cs
```

والمخرج سوف يكون كما يلي:

```text
1) Value Object
  a == b: True, same object: False
  a.Add(25) = 75.00 SAR, a is still 50.00 SAR
  REFUSED add USD to SAR: Cannot add USD to SAR
2) Entity identity
  same lines, same order? False
3) Aggregate rules (invariants)
  OK      add 3 x KB-9 at 120 SAR
  OK      add 2 more KB-9
  REFUSED add 1 more KB-9: At most 5 of KB-9 per order (asked 6)
  REFUSED add a line priced in USD: All lines of one order use one currency
  REFUSED place an empty order: An empty order cannot be placed
  total = 600.00 SAR, status = Draft
4) Domain Event
  REFUSED add a line after placing: A placed order cannot change
  event: OrderPlaced total=600.00 SAR
  shipping created: 2 parcels to Building 7, Example Street
```

وسوف نشرح هذا المخرج جزءاً جزءاً في الأقسام التالية.

### الكيان Entity وكائن القيمة Value Object

الكيان Entity كائن يعرف بهويته وليس بصفاته، فطلب رقم A17 يبقى نفس الطلب وهو في حالة المسودة بسطر واحد، ثم بثلاثة سطور، ثم بعد تأكيده، ولو تغيرت كل صفاته، ويقول Evans إن النموذج يجب أن يحدد ما معنى أن يكون الشيء هو نفسه. أما كائن القيمة Value Object فيعرف بقيمته فقط ولا هوية له، فورقة 50 ريالاً تساوي أي ورقة 50 ريالاً أخرى، ولا يهمك أي واحدة منهما في جيبك، ويطلب Evans أن يكون كائن القيمة غير قابل للتغيير Immutable وأن تكون عملياته دون آثار جانبية. والصورة التالية تبين الفرق:

![في اليمين ENTITY: ثلاث بطاقات للطلب Id = A17 في الساعة 10:00 وDraft بسطر واحد، ثم 10:05 وDraft بثلاثة سطور، ثم 10:07 وPlaced، وتحتها سهم الزمن. وفي اليسار VALUE OBJECT: ورقتا 50 مكتوب تحتهما Money(50, SAR) وبينهما ==، وتحتهما a.Add(25 SAR) returns a new Money(75, SAR) وa is still Money(50, SAR) مع قفل immutable](https://arabroot.io/content/images/2026/10/ddd-basics-04-entity-value.webp)

الكيان يبقى هو نفسه مع تغير صفاته، وكائن القيمة يتساوى بقيمته ولا يتغير

وفي المخرج أعلاه لاحظ التالي:

- `a == b` أعطت `True` مع أنهما كائنان مختلفان في الذاكرة (`same object: False`)، والسبب أن `Money` معرف بالكلمة `record`، وتقول Microsoft في [توثيق records](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record?ref=arabroot.io) إن ال record يأتي بالمساواة بالقيمة Value Equality جاهزة، أما ال class العادي فيتساوى فقط إذا كان نفس الكائن.
- `a.Add(25)` أعادت كائناً جديداً قيمته 75، وبقي `a` كما هو 50، وهذا هو عدم التغيير، ويشرح Fowler في مقاله [ValueObject](https://martinfowler.com/bliki/ValueObject.html?ref=arabroot.io) لماذا يهم ذلك: إذا كان كائن القيمة قابلاً للتعديل وكان متغيران يشيران إليه، فتعديله من مكان يغيره في المكان الآخر دون أن تقصد، ويسمي هذا Aliasing Bug.
- جمع ريال مع دولار رفضه الكائن نفسه، أي أن القاعدة تعيش مع البيانات التي تخصها بدل أن تتكرر في كل مكان يجمع فيه أحد مبلغين.
- طلبان بنفس السطور ليسا نفس الطلب (`same lines, same order? False`)، لأن الطلب كيان له `Id` خاص به.

والفكرة ليست خاصة بلغة C#، فنفس كائن القيمة في Python يكون `dataclass` مجمداً كما يلي:

```python
# The same Money value object in Python: a frozen dataclass. Run: python money.py
from dataclasses import dataclass, FrozenInstanceError
from decimal import Decimal

@dataclass(frozen=True)
class Money:
    amount: Decimal
    currency: str

    def __post_init__(self):
        if self.amount < 0:
            raise ValueError("Money cannot be negative")

    def add(self, other: "Money") -> "Money":
        if other.currency != self.currency:
            raise ValueError(f"Cannot add {other.currency} to {self.currency}")
        return Money(self.amount + other.amount, self.currency)

a = Money(Decimal("50"), "SAR")
b = Money(Decimal("50"), "SAR")
print("a == b:", a == b, "| same object:", a is b)
print("in a set:", len({a, b}), "item")
print("a.add(25):", a.add(Money(Decimal("25"), "SAR")), "| a:", a)
try:
    a.amount = Decimal("1")
except FrozenInstanceError as e:
    print("change a.amount:", type(e).__name__, e)
try:
    a.add(Money(Decimal("10"), "USD"))
except ValueError as e:
    print("add USD:", e)
```

وعند تشغيل الأمر `python money.py` سوف تجد أن المخرج كما يلي:

```text
a == b: True | same object: False
in a set: 1 item
a.add(25): Money(amount=Decimal('75'), currency='SAR') | a: Money(amount=Decimal('50'), currency='SAR')
change a.amount: FrozenInstanceError cannot assign to field 'amount'
add USD: Cannot add USD to SAR
```

لاحظ أن الكائنين المتساويين أصبحا عنصراً واحداً داخل ال Set، لأن `frozen=True` يجعل ال dataclass قابلاً للهاش Hashable حسب قيمه كما في [توثيق dataclasses](https://docs.python.org/3/library/dataclasses.html?ref=arabroot.io)، ولاحظ أن محاولة تعديل الحقل انتهت بالخطأ `FrozenInstanceError`. وتذكر أن الكيان والكائن مفهومان في البرمجة الكائنية OOP التي شرحناها في [أنماط البرمجة](https://arabroot.io/articles/%D8%A7%D9%86%D9%85%D8%A7%D8%B7-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%88%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#oop)، والجديد هنا هو السؤال الذي تسأله لكل مفهوم: هل تهمني هويته، أم تهمني قيمته فقط؟ وأغلب الفرق تكتب كيانات أكثر من حاجتها، والنصيحة أن تجعل كل شيء كائن قيمة ما لم تجد سبباً لهوية، مثل العنوان والمبلغ والفترة الزمنية والبريد الإلكتروني.

### ال Aggregate: حدود القواعد والمعاملة الواحدة

في المتجر قاعدة تقول إن العميل لا يطلب أكثر من خمس قطع من نفس الصنف في الطلب الواحد، فأين تكتب هذه القاعدة؟ الطريقة البسيطة أن تكتبها في الشاشة أو في ال Controller، ثم يأتي API جديد للتطبيق المحمول ويضيف السطور مباشرة فلا يمر بالقاعدة، أو تتحقق منها في دالة الإضافة بينما يعدل كود آخر الكمية في السطر نفسه مباشرة، وفي الحالتين تصبح القاعدة مكتوبة لكنها غير محروسة.

وال Aggregate هو الحل في DDD: مجموعة من الكيانات وكائنات القيمة تعامل كوحدة واحدة، ولها كيان رئيسي واحد اسمه ال Aggregate Root، ولا يمسك أحد من خارجها إلا هذا الكيان الرئيسي، فهو الذي يتحقق من القواعد الثابتة Invariants للمجموعة كلها. ويقول Fowler في مقاله [DDD\_Aggregate](https://martinfowler.com/bliki/DDD%5FAggregate.html?ref=arabroot.io) إن الطلب مع سطوره مثال واضح على ذلك، وإن ال Aggregate هو الوحدة التي تحمل وتحفظ كاملة، وإن المعاملة Transaction لا يجب أن تتجاوز حدوده. والصورة التالية تبين حدود ال Aggregate في متجرنا:

![حد منقط باسم ORDER AGGREGATE فيه Order مكتوب فوقه ROOT وId = A17، وتحته ثلاثة OrderLine لكل منها Money، ومكتوب في الأسفل معاملة واحدة تحفظ الطلب وسطوره معاً. في الخارج Customer يشير إلى الطلب بخط منقط CustomerId only، وProduct بخط SKU only. وفي اليمين API / Application يستدعي order.AddLine بسهم أخضر إلى ال Root، وسهم أحمر منقط عليه علامة X إلى OrderLine مكتوب عليه order.Lines.Add، ومربع فيه أربع قواعد](https://arabroot.io/content/images/2026/10/ddd-basics-05-aggregate.webp)

كل تعديل يمر عبر ال Aggregate Root، والإشارة إلى أي Aggregate آخر بمعرفه فقط

وفي الكود أعلاه لاحظ التالي:

- السطور محفوظة في قائمة خاصة `_lines`، والخارج يرى `IReadOnlyList` فقط، والطريقة الوحيدة للإضافة هي `AddLine` التي تتحقق من الكمية والعملة وحالة الطلب، لذلك رفضت القطعة السادسة من `KB-9` برسالة `asked 6` مع أن كل استدعاء وحده كان صغيراً.
- الطلب يشير إلى العميل بـ `CustomerId` وإلى المنتج بـ `Sku` فقط، ولا يحمل كائن العميل أو المنتج، والسبب أن العميل Aggregate آخر له قواعده ومعاملته، ولو حملته داخل الطلب لأصبح حفظ الطلب يقفل بيانات العميل أيضاً.
- القواعد التي تمتد على أكثر من Aggregate، مثل «لا تتجاوز مشتريات العميل حداً شهرياً»، لا تحاول فرضها في معاملة واحدة، وإنما بالأحداث كما في القسم التالي. ويقول Evans في الوثيقة المرجعية إن القواعد داخل حدود ال Aggregate تطبق فوراً، وبين ال Aggregates تطبق بشكل غير متزامن Asynchronous.
- اجعل ال Aggregate صغيراً، فكل ما بداخله يحمل ويحفظ ويقفل معاً، وال Aggregate الكبير معناه تعارض بين المستخدمين على نفس السجلات وبطء في كل عملية.

والآن لنكسر القاعدة عمداً: لو حاول كود خارجي أن يضيف سطراً مباشرة دون المرور بال Root، أي أضفنا قبل `order.Place()` السطر التالي:

```csharp
order.Lines.Add(new OrderLine("KB-9", 50, new Money(1m, "SAR")));
```

فسوف يرفض المترجم البرنامج قبل أن يعمل، والمخرج سوف يكون كما يلي:

```text
ddd.cs(132,21): error CS1061: 'IReadOnlyList<OrderLine>' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'IReadOnlyList<OrderLine>' could be found (are you missing a using directive or an assembly reference?)

The build failed. Fix the build errors and run again.
```

وهذا أفضل مكان يمكن أن يظهر فيه الخطأ، لأن القاعدة أصبحت جزءاً من شكل الكود وليست تعليقاً يقرؤه المطور أو لا يقرؤه. ولاحظ أن الخطأ ذكر `Add` فقط ولم يشتك من `new OrderLine`، والسبب أن المنشئ Constructor معرف بـ `internal` وكل الكود هنا في ملف واحد، لذلك في المشروع الحقيقي ضع نموذج المجال في مشروع Project منفصل حتى يصبح `internal` حاجزاً فعلياً.

### الأحداث Domain Events: شيء حدث ويهم أهل العمل

الحدث Domain Event هو شيء حدث في المجال ويهم خبراء المجال، ويطلب Evans أن تمثله ككائن في النموذج، وتقول Microsoft إن الحدث شيء حدث في الماضي، لذلك نسميه بصيغة الماضي: `OrderPlaced` وليس `PlaceOrder`، لأن الأمر Command طلب قد يرفض، أما الحدث فواقعة حصلت. وفي الكود أعلاه دالة `Place` تتحقق من القواعد ثم تغير الحالة وتضيف `OrderPlaced` إلى قائمة الأحداث، ولا تعرف شيئاً عن الشحن أو الفوترة، ثم يأخذ سياق الشحن هذا الحدث ويترجمه إلى لغته: `Shipment` وعدد طرود، دون أن يهتم بالسعر. والصورة التالية تبين الطريق الكامل للحدث في نظام حقيقي:

![مسار مرقم: 1 المستخدم يرسل الطلب من المتصفح، 2 Order.Place داخل SALES CONTEXT يسجل ورقة OrderPlaced، 3 حفظ الطلب والحدث في قاعدة بيانات orders + outbox في معاملة واحدة Transactional Outbox، 4 إرسال الحدث إلى broker / queue، ثم في منطقة LATER, ASYNC الخطوة 5 Shipping ينشئ new Shipment والخطوة 6 Billing ينشئ new Invoice](https://arabroot.io/content/images/2026/10/ddd-basics-06-domain-event.webp)

داخل ال Aggregate الاتساق فوري، وبين السياقات يصل الحدث بعد لحظات

وتفرق Microsoft في صفحة [Domain events: design and implementation](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-events-design-implementation?ref=arabroot.io) بين نوعين: الحدث داخل نفس المجال Domain Event الذي يعالج غالباً داخل نفس العملية Process وقد يكون متزامناً أو غير متزامن، وحدث التكامل Integration Event الذي يخرج إلى سياقات أو أنظمة أخرى ويجب أن يكون غير متزامن، ولا يرسل إلا بعد نجاح حفظ البيانات، وإلا كأن العملية لم تحدث. وهنا نقطتان عمليتان:

- إذا حفظت الطلب ثم أرسلت الحدث إلى ال Broker في خطوتين، فقد يتعطل السيرفر بينهما فيضيع الحدث، أو يرسل الحدث ثم يفشل الحفظ، والحل المعروف هو نمط Outbox الذي شرحناه في [الأنظمة الموزعة للمبتدئين](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D9%85%D9%88%D8%B2%D8%B9%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-cap-%D9%88%D8%A7%D9%84%D8%A7%D8%AA%D8%B3%D8%A7%D9%82-%D9%88%D8%A7%D9%84%D8%A7%D8%AC%D9%85%D8%A7%D8%B9/#outbox): تحفظ الحدث في جدول داخل نفس المعاملة، ثم يرسله عامل منفصل.
- الشحن والفوترة سوف يعرفان بالطلب بعد لحظات وليس في نفس اللحظة، وهذا هو الاتساق النهائي Eventual Consistency، وعلى الواجهة أن تتعامل معه، فتقول للمستخدم «تم استلام طلبك» ولا تعرض رقم الفاتورة فوراً.

وفكرة النشر والاشتراك هنا قريبة من نمط Observer الذي شرحناه في [أنماط التصميم](https://arabroot.io/articles/%D8%A7%D9%86%D9%85%D8%A7%D8%B7-%D8%A7%D9%84%D8%AA%D8%B5%D9%85%D9%8A%D9%85-design-patterns-%D8%A7%D9%84%D8%AA%D9%8A-%D8%AA%D9%82%D8%A7%D8%A8%D9%84%D9%87%D8%A7-%D9%81%D8%B9%D9%84%D8%A7/#observer)، وعندما تصبح الأحداث هي طريقة التواصل الرئيسية بين أجزاء النظام كله فهذا نمط معماري كامل نشرحه في [المعمارية الموجهة بالأحداث Event-Driven](https://arabroot.io/articles/%D8%A7%D9%86%D9%85%D8%A7%D8%B7-%D8%A7%D9%84%D9%85%D8%B9%D9%85%D8%A7%D8%B1%D9%8A%D8%A9-layered-%D9%88-event-driven-%D9%88-modular-monolith/#event-driven).

### ال Repository وال Factory والخدمات Domain Services

تكمل الوثيقة المرجعية الأنماط التكتيكية بثلاثة أنماط لن نفصلها هنا لأن لها مكاناً آخر في الموقع:

- **ال Repository**: واجهة تحمل وتحفظ Aggregate كاملاً كأنه في قائمة في الذاكرة، وتخفي قاعدة البيانات عن نموذج المجال، وتنصح Microsoft في صفحة [Designing the infrastructure persistence layer](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design?ref=arabroot.io) بـ Repository واحد لكل Aggregate، فلا يوجد `OrderLineRepository` لأن السطر لا يحفظ وحده. وشرحنا النمط نفسه في [أنماط التصميم](https://arabroot.io/articles/%D8%A7%D9%86%D9%85%D8%A7%D8%B7-%D8%A7%D9%84%D8%AA%D8%B5%D9%85%D9%8A%D9%85-design-patterns-%D8%A7%D9%84%D8%AA%D9%8A-%D8%AA%D9%82%D8%A7%D8%A8%D9%84%D9%87%D8%A7-%D9%81%D8%B9%D9%84%D8%A7/#repository).
- **ال Factory**: عندما يكون إنشاء Aggregate صحيح معقداً، تضع منطق الإنشاء في مكان واحد بدل أن يتكرر، وشرحناه في [نمط Factory](https://arabroot.io/articles/%D8%A7%D9%86%D9%85%D8%A7%D8%B7-%D8%A7%D9%84%D8%AA%D8%B5%D9%85%D9%8A%D9%85-design-patterns-%D8%A7%D9%84%D8%AA%D9%8A-%D8%AA%D9%82%D8%A7%D8%A8%D9%84%D9%87%D8%A7-%D9%81%D8%B9%D9%84%D8%A7/#factory).
- **الخدمة Domain Service**: عملية مهمة في المجال لا تخص كياناً واحداً بطبيعتها، مثل حساب سعر الشحن من عدة عوامل، فتضعها في خدمة بلا حالة Stateless باسم من اللغة المشتركة، بدل أن تحشرها في كيان لا علاقة له بها.

## أين يعيش نموذج المجال في معمارية التطبيق؟

إذا كان نموذج المجال يستدعي قاعدة البيانات أو مكتبة HTTP مباشرة، فسوف تتغير قواعد العمل كلما تغيرت التقنية، ولن تستطيع اختبار القواعد دون تشغيل قاعدة بيانات. لذلك تقول Microsoft في صفحة [Design a DDD-oriented microservice](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice?ref=arabroot.io) إن طبقة البنية التحتية Infrastructure تعتمد على طبقة المجال Domain، وطبقة المجال لا تعتمد على أي طبقة. ولاحظ أن الكود الذي كتبناه أعلاه لا يعرف شيئاً عن قاعدة بيانات أو إطار عمل، وهذا هو اتجاه الاعتماديات Dependency Direction الذي نشرحه بالتفصيل في [قاعدة الاعتماديات](https://arabroot.io/articles/clean-architecture-%D9%88-hexagonal-%D8%A7%D8%AA%D8%AC%D8%A7%D9%87-%D8%A7%D9%84%D8%A7%D8%B9%D8%AA%D9%85%D8%A7%D8%AF%D9%8A%D8%A7%D8%AA/#dependency-rule) وفي [Hexagonal Architecture](https://arabroot.io/articles/clean-architecture-%D9%88-hexagonal-%D8%A7%D8%AA%D8%AC%D8%A7%D9%87-%D8%A7%D9%84%D8%A7%D8%B9%D8%AA%D9%85%D8%A7%D8%AF%D9%8A%D8%A7%D8%AA/#hexagonal)، حيث يصبح نموذج المجال في المركز وقاعدة البيانات وال Broker مجرد Adapters حوله. ويمكنك اختبار كل القواعد التي رأيتها في المخرج باختبارات وحدة Unit Tests سريعة كما في [أنواع الاختبارات](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-unit-%D9%88-integration-%D9%88-e2e/#unit)، دون قاعدة بيانات أو شبكة.

## متى يكون DDD أكبر من حاجتك؟

الأنماط التكتيكية لها ثمن واضح: كلاسات أكثر، وتحويل بين نموذج المجال وجداول قاعدة البيانات، وأحداث يجب أن تتعامل مع وصولها المتأخر، وفريق يحتاج وقتاً حتى يتفق على اللغة. وتقول Microsoft صراحة في صفحة [Design a DDD-oriented microservice](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice?ref=arabroot.io) إن أنماط DDD تطبق فقط على الخدمات المعقدة التي فيها قواعد عمل كثيرة، وإن المسؤوليات البسيطة مثل خدمة CRUD تدار بطرق أبسط، وفي صفحة [Design a microservice domain model](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/microservice-domain-model?ref=arabroot.io) تذكر أن تطبيقها المرجعي eShopOnContainers طبق أنماط DDD في خدمة الطلبات Ordering ولم يطبقها في خدمة الكتالوج Catalog لأنها CRUD بسيطة. والصورة التالية تلخص القرار:

![مسار قرار من اليمين: نظام جديد، ثم السؤال 1 قواعد عمل معقدة تتغير مع الأعمال؟ لا يؤدي إلى CRUD، ثم السؤال 2 خبير بالمجال تتحدث معه باستمرار؟ لا يؤدي إلى LANGUAGE FIRST، ثم السؤال 3 هل هو ال Core Domain الذي يميز الشركة؟ لا يؤدي إلى KEEP IT SIMPLE، ونعم يؤدي إلى FULL DDD مع Aggregates وDomain Events وContext Map وEventStorming، وفي الأسفل شريط يقول في كل الحالات سم الأشياء بلغة أهل العمل وارسم حدود كل جزء من النظام](https://arabroot.io/content/images/2026/10/ddd-basics-07-when-ddd.webp)

الأنماط التكتيكية للأجزاء المعقدة فقط، واللغة والحدود لكل نظام

وبشكل عملي، لا تستخدم الأنماط التكتيكية في الحالات التالية:

- **تطبيق CRUD**: شاشات تضيف وتعدل وتحذف وتعرض بيانات بلا قواعد تذكر، مثل لوحة إدارة المحتوى أو دليل الموظفين، وهنا يكفي نموذج بسيط وطبقات واضحة، ويسمي البعض هذا النموذج الذي يحمل البيانات بلا سلوك Anemic Domain Model، وتقول Microsoft إنه قد يكون كافياً تماماً للخدمة البسيطة.
- **مجال عام Generic**: الدخول والصلاحيات والبريد والدفع، فاستخدم أداة أو خدمة جاهزة واحمها بطبقة ACL.
- **نموذج أولي Prototype أو مشروع قصير العمر**: لم تفهم المجال بعد، والكود سوف يرمى غالباً، فلا تبن Aggregates لقواعد لم تتضح، وهذا تطبيق لمبدأ [YAGNI](https://arabroot.io/articles/%D9%85%D8%A8%D8%A7%D8%AF%D8%A6-%D8%A7%D9%84%D8%AA%D8%B5%D9%85%D9%8A%D9%85-solid-%D9%88-dry-%D9%88-kiss/#yagni).
- **لا يوجد خبير بالمجال**: DDD قائم على الحديث مع من يفهم العمل، فإذا لم يتوفر هذا الشخص، فابدأ باللغة والحدود فقط.

أما الأخطاء الشائعة التي نراها عند من يطبق DDD لأول مرة فهي:

- **DDD كترتيب مجلدات**: مجلدات `Entities` و`ValueObjects` و`Aggregates` بأسماء تقنية، دون أي حديث مع أهل العمل، وهذا يأخذ الثمن كله ويترك الفائدة.
- **Aggregate كبير جداً**: العميل وكل طلباته وكل فواتيره في Aggregate واحد، فيقفل حفظ أي طلب كل شيء يخص العميل.
- **خدمة مصغرة لكل كيان**: خدمة للطلب وخدمة لسطر الطلب وخدمة للعميل، والحدود الصحيحة هي حدود السياقات وليس حدود الجداول، وإلا ظهر ما يسمى النظام الموزع المترابط Distributed Monolith.
- **Shared Kernel يكبر بلا توقف**: مكتبة مشتركة تبدأ بـ `Money` وتنتهي بنصف النموذج، فيعود الترابط Coupling الذي شرحناه في [مبادئ التصميم](https://arabroot.io/articles/%D9%85%D8%A8%D8%A7%D8%AF%D8%A6-%D8%A7%D9%84%D8%AA%D8%B5%D9%85%D9%8A%D9%85-solid-%D9%88-dry-%D9%88-kiss/#coupling-cohesion) من باب خلفي.
- **بناء المجال العام بنفسك**: فريق يقضي ثلاثة أشهر في نظام صلاحيات خاص بينما المجال الأساسي ينتظر.

## تدريب عملي

1. خذ نظاماً تعمل عليه الآن، واكتب قاموساً من عشر كلمات يستخدمها أهل العمل، ثم ابحث عن كل كلمة في الكود: كم كلمة تجدها بنفس الاسم؟ وكم كلمة لها معنيان بحسب القسم؟ الكلمات ذات المعنيين هي أماكن الحدود المحتملة بين السياقات.
2. أضف إلى ملف `ddd.cs` قاعدة جديدة: لا يتجاوز مجموع الطلب 10000 ريال، ثم جرب أن تضيف سطوراً تتجاوزها، وفكر أين يجب أن تكتب القاعدة حتى لا يستطيع أي كود آخر تجاوزها.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- التصميم الموجه بالمجال DDD طريقة تفكير تبدأ من فهم المجال مع أهله، وجانبه الاستراتيجي (اللغة والحدود والعلاقات) يفيد أغلب الفرق، أما جانبه التكتيكي فيفيد الأجزاء المعقدة فقط.
- صنف المجالات الفرعية إلى Core وSupporting وGeneric، وضع أفضل جهدك في ال Core، واستخدم حلولاً جاهزة لل Generic.
- اللغة المشتركة Ubiquitous Language تعني نفس الكلمات في الاجتماع والكود والاختبارات، والسياق المحدد Bounded Context هو الحد الذي تبقى داخله الكلمة بمعنى واحد.
- خريطة السياقات Context Map توثق العلاقات بين السياقات والفرق، وأهم علاقة عملياً هي طبقة ACL أمام كل نظام لا تملكه.
- الكيان Entity يعرف بهويته، وكائن القيمة Value Object يعرف بقيمته ولا يتغير، وال Aggregate يحرس قواعده عبر ال Aggregate Root ويحفظ في معاملة واحدة، والأحداث Domain Events تنقل ما حدث إلى السياقات الأخرى مع اتساق نهائي.
- لا تطبق الأنماط التكتيكية على تطبيق CRUD أو نموذج أولي أو مجال عام، فهي في هذه الحالات تعقيد بلا فائدة.

وأقرب المقالات إلى هذا الموضوع على الموقع:

[![](https://arabroot.io/content/images/2026/10/arch-intro-cover-cover.webp)دليلالتصميم والمعمارية: أربعة مستويات لنشاط واحدما الفرق بين تصميم البرمجيات ومعماريتها وتصميم الأنظمة ومعمارية المؤسسة، ومن يقرر في كل مستوى وكم يعيش قراره.](https://arabroot.io/articles/%D8%A7%D9%84%D9%81%D8%B1%D9%82-%D8%A8%D9%8A%D9%86-%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A7%D8%AA-%D9%88%D9%85%D8%B9%D9%85%D8%A7%D8%B1%D9%8A%D8%AA%D9%87%D8%A7-%D9%88%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9/)[![](https://arabroot.io/content/images/2026/10/clean-architecture-cover-cover.webp)دليلClean Architecture: كيف تعزل منطق العمل عن قاعدة البياناتلماذا تحتاج قاعدة بيانات لكي تختبر قاعدة عمل من سطر واحد، وكيف يعزل اتجاه الاعتماديات منطق العمل عن التفاصيل.](https://arabroot.io/articles/clean-architecture-%D9%88-hexagonal-%D8%A7%D8%AA%D8%AC%D8%A7%D9%87-%D8%A7%D9%84%D8%A7%D8%B9%D8%AA%D9%85%D8%A7%D8%AF%D9%8A%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/architecture-styles-cover-cover.webp)دليلأنماط المعمارية Architecture Styles ومتى تختار كلاً منهاكيف تختار بين التطبيق الواحد والخدمات التي تتبادل الأحداث، وما الذي يناسب نظامك من أشهر أنماط المعمارية.](https://arabroot.io/articles/%D8%A7%D9%86%D9%85%D8%A7%D8%B7-%D8%A7%D9%84%D9%85%D8%B9%D9%85%D8%A7%D8%B1%D9%8A%D8%A9-layered-%D9%88-event-driven-%D9%88-modular-monolith/)[![](https://arabroot.io/content/images/2026/10/design-patterns-cover-cover.webp)دليلأنماط التصميم Design Patterns التي تقابلها فعلاًأشهر أنماط التصميم التي تقابلها في كل مشروع، والمشكلة التي يحلها كل نمط، ومتى يصبح عبئاً على من يعدل الكود بعدك.](https://arabroot.io/articles/%D8%A7%D9%86%D9%85%D8%A7%D8%B7-%D8%A7%D9%84%D8%AA%D8%B5%D9%85%D9%8A%D9%85-design-patterns-%D8%A7%D9%84%D8%AA%D9%8A-%D8%AA%D9%82%D8%A7%D8%A8%D9%84%D9%87%D8%A7-%D9%81%D8%B9%D9%84%D8%A7/)[![](https://arabroot.io/content/images/2026/10/architecture-docs-cover-cover.webp)دليلتوثيق المعمارية: مخططات C4 وسجلات القرارات المعمارية ADRكيف ترسم نظامك بمخططات C4 يفهمها المطور الجديد، وتحفظ سبب كل قرار مهم في سجل ADR داخل المستودع.](https://arabroot.io/articles/%D8%AA%D9%88%D8%AB%D9%8A%D9%82-%D8%A7%D9%84%D9%85%D8%B9%D9%85%D8%A7%D8%B1%D9%8A%D8%A9-%D9%85%D8%AE%D8%B7%D8%B7%D8%A7%D8%AA-c4-%D9%88-adr/)[![](https://arabroot.io/content/images/2026/10/distributed-systems-cover-cover.webp)دليلالأنظمة الموزعة للمبتدئين: ماذا يحدث عندما تنقطع الشبكةلماذا تتصرف الأنظمة الموزعة بشكل غريب عند انقطاع الشبكة، وكيف تتفق السيرفرات على قرار واحد رغم ذلك.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D9%85%D9%88%D8%B2%D8%B9%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-cap-%D9%88%D8%A7%D9%84%D8%A7%D8%AA%D8%B3%D8%A7%D9%82-%D9%88%D8%A7%D9%84%D8%A7%D8%AC%D9%85%D8%A7%D8%B9/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، ومراجعة التعريفات من وثيقة DDD Reference لعام 2015 ومقالات Martin Fowler وإرشادات Microsoft، وتشغيل الأمثلة على .NET SDK 10.0.400 وPython 3.13.3.