دليل تقني

التصميم الموجه بالمجال في الممارسة: كيف نرسم حدود الوحدات

أنماط DDD عملية لـ TypeScript و NestJS. رسم حدود الوحدات، EventBus كطبقة مكافحة الفساد، متى DDD مبالغة، النواة المشتركة، وإعادة الهيكلة نحو الحدود.

6 فبراير 202614 دقيقة للقراءةفريق هندسة أورنتس

DDD مش عن الكتاب الأزرق

أغلب مقالات DDD تبدأ بالـ Aggregates و Value Objects و Domain Events من كتاب Eric Evans. هذي نظريات. في الواقع العملي، أهم مفهوم من DDD هو شي واحد: الحدود.

وين ينتهي الموديول A ووين يبدأ الموديول B؟ أي بيانات يملكها كل موديول؟ كيف يتواصلون؟ لما ترسم الحدود صح، الكودبيس تبقى نظيفة وهي تكبر. لما ترسمها غلط، كل ميزة تلمس كل موديول، وإعادة الهيكلة تصير مستحيلة.

رسمنا حدود وحدات في عدة أنظمة إنتاج: Vendure plugin من 6 وحدات، منصة تذاكر من 8 خدمات، و PIM فيه 13 مشترك أحداث وتنسيق workers معقد. هالمقال يغطي الأنماط العملية. للتواصل المبني على الأحداث عبر الحدود، شوف دليل المعمارية المبنية على الأحداث. لأنماط Vendure تحديداً، شوف دليل معمارية Vendure plugins.

قاعدة الاعتماديات

أبسط طريقة لتحديد حدود الوحدات: ارسم رسم بياني للاعتماديات. إذا الموديول A يستورد شي من الموديول B والموديول B يستورد شي من الموديول A، عندك اعتمادية دائرية. الحد غلط.

// سيء: اعتماديات دائرية
WishlistService يستورد LoyaltyService   (لمنح النقاط)
LoyaltyService يستورد WishlistService   (للتحقق من الـ wishlist للمكافأة)

// جيد: تواصل مبني على الأحداث
WishlistService يبث WishlistItemAddedEvent
LoyaltyService يشترك في WishlistItemAddedEvent (يمنح النقاط)
LoyaltyService يبث PointsAwardedEvent
WishlistService ما يهمه (ما يحتاج اشتراك)

القاعدة: الاعتماديات تتدفق باتجاه واحد. إذا وحدتين تحتاج تتواصل بالاتجاهين، استخدم Events. الأحداث أحادية الاتجاه بالتصميم. الباث ما يعرف (وما يهمه) مين يشترك.

كيف تتحقق

# إيجاد الاستيرادات الدائرية في مشروع TypeScript
npx madge --circular src/

# إيجاد الاستيرادات عبر الوحدات
grep -rn "from '.*wishlist.*'" src/modules/loyalty/
grep -rn "from '.*loyalty.*'" src/modules/wishlist/

إذا هالأوامر ترجع نتائج، عندك انتهاكات حدود. حلها باستخراج الشغلة المشتركة لوحدة منفصلة أو استبدال الاستيراد بحدث.

EventBus كطبقة مكافحة الفساد

الـ EventBus مش بس نظام رسائل. هو طبقة مكافحة الفساد بين الوحدات. كل وحدة تنشر أحداث توصف شو صار في مجالها. وحدات ثانية تشترك وتترجم هالأحداث لمفاهيم مجالها الخاص.

// وحدة الـ Wishlist: تنشر حدث المجال
export class WishlistItemAddedEvent extends VendureEvent {
    constructor(
        public ctx: RequestContext,
        public wishlistId: string,
        public productVariantId: string,
        public customerId: string,
    ) {
        super();
    }
}

// خدمة الـ Wishlist: تبث حدث بعد منطق الأعمال
@Injectable()
export class WishlistService {
    constructor(private eventBus: EventBus) {}

    async addItem(ctx: RequestContext, input: AddItemInput): Promise<WishlistItem> {
        // منطق الأعمال: التحقق، فحص التكرارات، الحفظ
        const item = await this.saveItem(ctx, input);

        // نشر الحدث: "شي صار في مجالي"
        await this.eventBus.publish(
            new WishlistItemAddedEvent(ctx, input.wishlistId, input.productVariantId, ctx.activeUserId)
        );

        return item;
    }
}

// وحدة الـ Loyalty: تشترك وتترجم لمجالها الخاص
@Injectable()
export class LoyaltyEventHandler {
    constructor(
        private eventBus: EventBus,
        private loyaltyService: LoyaltyService,
    ) {
        // الاشتراك: ترجمة حدث الـ wishlist لمفهوم الـ loyalty
        this.eventBus.ofType(WishlistItemAddedEvent).subscribe(async event => {
            await this.loyaltyService.awardPoints(
                event.ctx,
                event.customerId,
                'wishlist_add',  // مفهوم خاص بالـ Loyalty، مش الـ Wishlist
                10,              // عدد النقاط (قرار الـ Loyalty)
            );
        });
    }
}

شو اللي يخلي هذا طبقة مكافحة فساد

  • وحدة الـ Wishlist ما تعرف إن الـ Loyalty موجود. تنشر WishlistItemAddedEvent بغض النظر عن المشتركين.
  • وحدة الـ Loyalty ما تستورد خدمات الـ Wishlist. تعتمد بس على كلاس الحدث.
  • عدد النقاط (10) هو قرار مجال الـ Loyalty، مش شغل الـ Wishlist.
  • إذا الـ Loyalty ينحذف، الـ Wishlist يستمر يشتغل بدون تغيير.
  • إذا وحدة جديدة (Analytics) تبي تتفاعل مع إضافات الـ Wishlist، تشترك بنفس الحدث. صفر تغييرات على الـ Wishlist.

بنية الوحدات في TypeScript/NestJS

وحدة منظمة بشكل جيد عندها تنظيم داخلي واضح:

src/modules/wishlist/
├── entities/
│   ├── wishlist.entity.ts
│   └── wishlist-item.entity.ts
├── services/
│   └── wishlist.service.ts
├── resolvers/
│   ├── wishlist-shop.resolver.ts
│   └── wishlist-admin.resolver.ts
├── events/
│   └── wishlist.events.ts
├── schemas/
│   ├── wishlist-shop.schema.ts
│   └── wishlist-admin.schema.ts
├── types/
│   └── wishlist.types.ts
└── wishlist.module.ts

تسجيل الوحدة

// wishlist.module.ts
@Module({
    imports: [TypeOrmModule.forFeature([CiWishlist, CiWishlistItem])],
    providers: [WishlistService, WishlistShopResolver, WishlistAdminResolver],
    exports: [WishlistService], // بس إذا وحدات ثانية فعلاً تحتاجه
})
export class WishlistModule {}

مصفوفة exports هي الـ API العامة للوحدة. صدّر بس اللي وحدات ثانية فعلاً تحتاجه. أغلب الوحدات لازم ما تصدر شي (تواصل عبر الأحداث بدالها).

شو يعبر حدود الوحدات

يعبر الحدكيف
الأحداثتُنشر عبر EventBus، وحدات ثانية تشترك فيها
معرفات الكياناتتمرر كقيم بسيطة (string/number)، مش مراجع كيانات
DTOs/Interfacesأنواع مشتركة لحمولات الأحداث
ما يعبر الحدليش
مثيلات الخدمةينشئ اقتران. استخدم الأحداث بدالها.
مراجع الكياناتالموديول A ما لازم يستعلم من جداول الموديول B.
الوصول للمستودعاتكل وحدة تملك بياناتها الخاصة.
الحالة الداخليةخاصة بالوحدة.

متى DDD مبالغة

مفاهيم DDD تضيف قيمة لما المجال معقد والفريق كبير. لكثير من التطبيقات، أنماط أبسط تشتغل أحسن.

الحالةDDD؟النهج الأفضل
تطبيق CRUD بقواعد أعمال بسيطةلانمط MVC/Service-Repository قياسي
2-3 مهندسين، سياق محدود واحدلامونوليث منظم جيداً بمجلدات واضحة
MVP لستارتب (Product-Market Fit مجهول)لاتحرك بسرعة، أعد الهيكلة لاحقاً لما المجال يستقر
نظام مؤسسة مع 6+ مجالات أعمال مختلفةنعمسياقات محدودة بحدود وحدات واضحة
فرق متعددة تشتغل على نفس الكودبيسنعمملكية الوحدات تتوافق مع ملكية الفريق
قواعد أعمال معقدة تختلف حسب السياقنعمنماذج المجال تلتقط القواعد بشكل صريح

الحل الوسط العملي

ما تحتاج Aggregates و Domain Events و Anti-Corruption Layers لمدونة بتعليقات. تحتاج حدود وحدات وتواصل مبني على الأحداث لمنصة تجارة إلكترونية فيها wishlist و reviews و loyalty و cart recovery و back-in-stock alerts.

الحل الوسط: نظم الكود في وحدات بحدود واضحة وتواصل عبر EventBus. لا تطبق أنماط DDD كاملة إلا إذا تعقيد المجال يبررها. ثلاث أسطر كود متشابهة أحسن من تجريد مبكر.

النواة المشتركة: الكود اللي ما يملكه أحد

بعض الكود مشترك فعلاً بين الوحدات. الثوابت، دوال المساعدة، الأنواع الأساسية، والواجهات المشتركة. هذي النواة المشتركة.

src/shared/
├── constants/
│   └── index.ts         # TABLE_NAMES، الصلاحيات، إلخ.
├── types/
│   └── common.types.ts  # DTOs مشتركة، أنواع ترقيم الصفحات
├── utils/
│   └── date.utils.ts    # تنسيق التواريخ، مساعدات المنطقة الزمنية
└── errors/
    └── common.errors.ts # كلاسات الأخطاء الأساسية

قواعد النواة المشتركة:

  • الحد الأدنى: بس اللي فعلاً يحتاج مشاركة. إذا يستخدمه وحدة واحدة، يخصها.
  • مستقر: تغييرات النواة المشتركة تأثر على كل الوحدات. لازم يتغير نادراً.
  • بدون منطق أعمال: بس أدوات مساعدة، أنواع، وثوابت. قواعد الأعمال تخص وحدات المجال.
  • ملك فريق المنصة (أو صراحةً ما يملكه أحد). إذا النواة المشتركة ما لها مالك، تصير مكب نفايات.

إعادة الهيكلة نحو الحدود

أغلب الكودبيسات ما تبدأ بحدود وحدات نظيفة. تتطور من مونوليث كل شي يستورد كل شي. إعادة الهيكلة نحو الحدود عملية تدريجية.

الخطوة 1: رسم خريطة الاعتماديات الحالية

# توليد رسم بياني للاعتماديات
npx madge --image dependency-graph.svg src/

# إيجاد الملفات الأكثر استيراداً (نقاط ساخنة للاقتران)
grep -rn "import.*from" src/ --include="*.ts" | awk -F"from " '{print $2}' | sort | uniq -c | sort -rn | head 20

الخطوة 2: تحديد الحدود الطبيعية

ابحث عن مجموعات ملفات تتغير مع بعض. إذا wishlist.service.ts و wishlist.entity.ts و wishlist.resolver.ts دائماً تتغير في نفس الـ PR، هم وحدة طبيعية.

الخطوة 3: استخراج وحدة واحدة بالمرة

التكرار 1: نقل ملفات الـ wishlist إلى src/modules/wishlist/
التكرار 2: استبدال الاستيرادات المباشرة بأحداث
التكرار 3: نقل ملفات الـ loyalty إلى src/modules/loyalty/
التكرار 4: استبدال الاستيرادات المباشرة بأحداث
...

لا تحاول تستخرج كل الوحدات مرة واحدة. استخرج واحدة، تحقق إنها تشتغل، ثم استخرج الجاية. كل استخراج لازم يكون PR منفصل بتيستاته الخاصة.

الخطوة 4: فرض الحدود

// قاعدة ESLint لمنع الاستيرادات عبر الوحدات
// .eslintrc.js
module.exports = {
    rules: {
        'no-restricted-imports': ['error', {
            patterns: [
                {
                    group: ['*/modules/loyalty/*'],
                    message: 'وحدة الـ Wishlist ما تقدر تستورد من الـ Loyalty. استخدم EventBus.',
                },
            ],
        }],
    },
};

افرض الحدود بالـ Linting، مش بالتوثيق. التوثيق ينتجاهل. أخطاء الـ Lint تحجز الـ PRs.

أخطاء شائعة

  1. البدء بـ DDD بدل إعادة الهيكلة نحوه. في مشروع جديد، ابدأ بسيط. أضف حدود وحدات لما تعقيد المجال يتطلبها. لا تصمم Aggregates لتطبيق CRUD.

  2. تصدير كل شي. إذا وحدة تصدر كل خدماتها، وحدات ثانية راح تستوردها. صدّر لا شي بالافتراض. استخدم الأحداث للتواصل عبر الوحدات.

  3. استعلامات قاعدة بيانات مشتركة عبر الوحدات. الموديول A ما لازم يكتب SQL يعمل join لجداول الموديول B. كل وحدة تملك بياناتها. إذا A يحتاج بيانات من B، الـ B يوفرها عبر حدث أو طريقة خدمة عامة.

  4. أحداث بكثير بيانات. الحدث لازم يحمل IDs وسياق أدنى، مش كيانات كاملة. المشترك يجيب اللي يحتاجه من مخزن بياناته الخاص.

  5. ما فيه فرض. حدود الوحدات بدون قواعد lint هي اقتراحات. الاقتراحات تنتهك تحت ضغط المواعيد. قواعد Lint تمنع الانتهاكات.

  6. التجريد المبكر. ثلاث دوال متشابهة في ثلاث وحدات عادي. استخراج تجريد مشترك قبل ما تفهم النمط ينشئ التجريد الغلط. انتظر للمرة الثالثة.

  7. مفردات DDD بدون فهم DDD. تسمية الأشياء "Aggregate" و "Value Object" بدون فهم الغرض هي هندسة Cargo Cult. الغرض هو التقاط قواعد المجال، مش إبهار مراجعي الكود.

النقاط الرئيسية

  • DDD عن الحدود، مش الأنماط. وين تنتهي وحدة ووين تبدأ أخرى؟ شو تملك كل وحدة؟ كيف يتواصلون؟ إذا هذي صح، الكودبيس تبقى نظيفة.

  • الأحداث هي طبقة مكافحة الفساد. الـ EventBus يمنع الاعتماديات الدائرية ويبقي الوحدات مستقلة. الباثون ما يعرفون عن المشتركين. التواصل أحادي الاتجاه.

  • صدّر لا شي بالافتراض. خدمات الوحدة خاصة إلا إذا فيه حاجة حقيقية لتصديرها. أغلب التواصل بين الوحدات لازم يمر عبر الأحداث.

  • ابدأ بسيط، أعد الهيكلة نحو الحدود. لا تصمم Aggregates في اليوم الأول. ابنِ الميزة، لاحظ الحدود الطبيعية، ثم استخرج الوحدات.

  • افرض بالـ Linting، مش بالتوثيق. قواعد ESLint اللي تمنع الاستيرادات عبر الوحدات أكثر فعالية من صفحات الويكي اللي توصف بنية الوحدات.

  • النواة المشتركة أدنى ومستقرة. ثوابت، أنواع، وأدوات مساعدة بس. بدون منطق أعمال. تغييرات النواة المشتركة تأثر على كل وحدة.

نطبق هالأنماط في مشاريع البرمجيات المخصصة وتطوير Vendure plugins. إذا تحتاج مساعدة بمعمارية الكودبيس، تواصل مع فريقنا أو اطلب عرض سعر. شوف كمان دليل هندسة البرمجيات لمبادئنا الهندسية الأوسع.

المواضيع المغطاة

DDD عمليالتصميم الموجه بالمجالسياقات محدودةمعمارية الوحداتDDD TypeScriptDDD NestJSحدود الوحداتطبقة مكافحة الفساد

جاهز لبناء أنظمة ذكاء اصطناعي جاهزة للإنتاج؟

فريقنا متخصص في بناء أنظمة ذكاء اصطناعي جاهزة للإنتاج. خلينا نحكي كيف نقدر نساعد.

ابدأ محادثة