هندسة البرمجيات

سياقات DDD المحددة: دليل التنفيذ الكامل

تعلم كيفية تنفيذ السياقات المحددة في التصميم المبني على النطاق. دليل عملي يغطي خرائط السياق والتجميعات وأحداث النطاق وأمثلة حقيقية.

Khalid Aboubakr
25 دقيقة قراءة
DddBounded ContextMicroservicesDomain ModelingUbiquitous LanguageAggregates

مقدمة

يظل التصميم الموجه بالنطاق (DDD) أحد أقوى الأدوات للتعامل مع التعقيد في برمجيات المؤسسات. ومع ذلك، فإن مفهوم السياقات المحدودة - وهو يمكن القول أنه المساهمة الأكثر قيمة لـ DDD - غالباً ما يُساء فهمه أو يُنفذ بشكل سيئ.

بعد تطبيق مبادئ DDD عبر أنظمة برمجيات الرعاية الصحية ومنصات الخدمات الحكومية وأنظمة اللوجستيات للمؤسسات، طورت نهجاً عملياً لتصميم السياقات المحدودة يوازن بين النقاء النظري والقيود الواقعية.

ما هو السياق المحدود؟

السياق المحدود هو حدود دلالية يطبق ضمنها نموذج نطاق معين. داخل هذه الحدود، للمصطلحات معانٍ محددة وغير غامضة. خارجها، قد تعني نفس المصطلحات شيئاً مختلفاً تماماً.

فكر في مصطلح "المريض" في نظام مستشفى:

  • في السياق السريري، للمريض تاريخ طبي وتشخيصات وخطط علاج
  • في سياق الفوترة، للمريض معلومات تأمين وتاريخ دفع وأرصدة مستحقة
  • في سياق الجدولة، للمريض تفضيلات مواعيد وقيود توفر

هذه ليست ثلاث طرق عرض لنفس النموذج - إنها ثلاثة نماذج مميزة تشترك في الاسم.

تحديد السياقات المحدودة

الجزء الأصعب في DDD هو معرفة أين ترسم الحدود. إليك عمليتي المجربة:

الخطوة 1: عصف الأحداث

اجمع خبراء النطاق والمطورين. ارسم خريطة لأحداث النطاق (الأشياء التي تحدث) على جدول زمني.

الجدول الزمني لزيارة الرعاية الصحية:
[تسجيل المريض] → [جدولة الموعد] → [تسجيل وصول المريض] → 
[تسجيل العلامات الحيوية] → [بدء الاستشارة] → [التشخيص] → 
[كتابة الوصفة] → [اكتمال الزيارة] → [إنشاء الفاتورة] → 
[استلام الدفع]

الخطوة 2: تحديد مجموعات اللغة

جمّع الأحداث حسب اللغة المستخدمة لوصفها. عندما يبدأ خبراء النطاق باستخدام مصطلحات مختلفة لمفاهيم متشابهة، فقد وجدت على الأرجح حدوداً.

// مجموعة اللغة السريرية interface ClinicalPatient { medicalRecordNumber: string; allergies: Allergy[]; currentMedications: Medication[]; problemList: Diagnosis[]; } // مجموعة لغة الفوترة interface BillingPatient { accountNumber: string; insurancePolicies: InsurancePolicy[]; paymentMethods: PaymentMethod[]; outstandingBalance: Money; }

الخطوة 3: رسم خريطة علاقات السياق

بمجرد تحديد السياقات، ارسم خريطة لكيفية ارتباطها.

أنماط رسم خرائط السياق

طبقة مكافحة الفساد (ACL)

عند التكامل مع الأنظمة القديمة أو الخدمات الخارجية التي لا تتبع نموذج نطاقك:

// طبقة مكافحة الفساد لنظام الفوترة القديم class BillingAntiCorruptionLayer { constructor(private legacyBillingClient: LegacyBillingClient) {} async createInvoice(clinicalVisit: ClinicalVisit): Promise<Invoice> { // ترجمة من نموذج نطاقنا إلى التنسيق القديم const legacyRequest = { PTNT_ID: clinicalVisit.patientId, VST_DT: this.formatLegacyDate(clinicalVisit.date), CHRG_CDS: clinicalVisit.procedures.map(p => this.mapToBillingCode(p)), DIAG_CDS: clinicalVisit.diagnoses.map(d => d.icd10Code) }; const legacyResponse = await this.legacyBillingClient.createCharge(legacyRequest); // الترجمة مرة أخرى إلى نموذج نطاقنا return new Invoice({ id: InvoiceId.fromLegacy(legacyResponse.CHRG_NBR), patientId: clinicalVisit.patientId, lineItems: this.translateLineItems(legacyResponse.LN_ITMS), totalAmount: Money.fromCents(legacyResponse.TOT_AMT) }); } }

النواة المشتركة

عندما يحتاج سياقان حقاً لمشاركة مجموعة فرعية صغيرة من نموذج النطاق:

// النواة المشتركة: أنواع مشتركة تستخدمها السياقات السريرية والمختبرية // هذا الكود يعيش في حزمة منفصلة ومُصدّرة بعناية export class PatientIdentifier { private constructor( public readonly medicalRecordNumber: string, public readonly facilityCode: string ) { this.validate(); } static create(mrn: string, facility: string): PatientIdentifier { return new PatientIdentifier(mrn, facility); } private validate(): void { if (!/^[A-Z]{2}\d{8}$/.test(this.medicalRecordNumber)) { throw new InvalidMRNError(this.medicalRecordNumber); } } }

تنفيذ السياقات المحدودة

هيكل الوحدات

يجب أن يكون كل سياق محدود وحدة مكتفية ذاتياً:

src/
├── clinical/
│   ├── domain/
│   │   ├── entities/
│   │   ├── value-objects/
│   │   ├── aggregates/
│   │   ├── repositories/
│   │   └── services/
│   ├── application/
│   │   ├── commands/
│   │   ├── queries/
│   │   └── handlers/
│   ├── infrastructure/
│   └── api/
├── billing/
│   └── ... (نفس الهيكل)
└── shared-kernel/
    └── PatientIdentifier.ts

تصميم التجميعات

تفرض التجميعات حدود الاتساق داخل السياق المحدود:

// تجميع المواجهة السريرية class ClinicalEncounter { private readonly events: DomainEvent[] = []; static create(patientId: PatientIdentifier): ClinicalEncounter { const encounter = new ClinicalEncounter( EncounterId.generate(), patientId, EncounterStatus.CREATED, null, [], [] ); encounter.addEvent(new EncounterCreated(encounter.id, patientId)); return encounter; } recordVitals(vitals: VitalSigns): void { this.ensureStatus(EncounterStatus.IN_PROGRESS); // قاعدة العمل: يمكن تحديث العلامات الحيوية لكن التاريخ يُحفظ const previousVitals = this.vitalSigns; this.vitalSigns = vitals; this.addEvent(new VitalsRecorded(this.id, vitals, previousVitals)); // التحقق من القيم الحرجة if (vitals.isCritical()) { this.addEvent(new CriticalVitalsDetected(this.id, vitals)); } } addDiagnosis(diagnosis: Diagnosis): void { this.ensureStatus(EncounterStatus.IN_PROGRESS); // قاعدة العمل: لا تكرار للتشخيصات if (this.diagnoses.some(d => d.code.equals(diagnosis.code))) { throw new DuplicateDiagnosisError(diagnosis.code); } this.diagnoses.push(diagnosis); this.addEvent(new DiagnosisAdded(this.id, diagnosis)); } complete(): void { // قاعدة العمل: يجب وجود تشخيص واحد على الأقل للإكمال if (this.diagnoses.length === 0) { throw new EncounterCannotBeCompletedError('يتطلب تشخيص واحد على الأقل'); } this.status = EncounterStatus.COMPLETED; this.addEvent(new EncounterCompleted(this.id, this.diagnoses, this.procedures)); } }

التواصل بين السياقات

متزامن: واجهات برمجة السياق

// سياق الفوترة يحتاج بيانات المريض الديموغرافية من السياق السريري class BillingService { constructor( private clinicalContextApi: ClinicalContextApi, private billingRepository: BillingAccountRepository ) {} async createAccount(patientId: PatientIdentifier): Promise<BillingAccount> { // استدعاء السياق السريري من خلال واجهته العامة const patientDemographics = await this.clinicalContextApi.getPatientDemographics(patientId); // الترجمة إلى نموذج سياق الفوترة const account = BillingAccount.create({ patientId, name: patientDemographics.fullName, dateOfBirth: patientDemographics.dateOfBirth, }); await this.billingRepository.save(account); return account; } }

غير متزامن: أحداث النطاق

// السياق السريري ينشر الأحداث class EncounterCompletedHandler { constructor(private eventPublisher: EventPublisher) {} async handle(encounter: ClinicalEncounter): Promise<void> { const events = encounter.pullEvents(); for (const event of events) { if (event instanceof EncounterCompleted) { await this.eventPublisher.publish('clinical.encounter.completed', { encounterId: event.encounterId.toString(), patientId: event.patientId.toString(), diagnoses: event.diagnoses.map(d => ({ code: d.code.toString(), description: d.description })), completedAt: event.timestamp.toISOString() }); } } } }

الأخطاء الشائعة وكيفية تجنبها

الخطأ 1: السياق المحدود = الخدمة المصغرة

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

الخطأ 2: مشاركة قواعد البيانات عبر السياقات

يجب أن يمتلك كل سياق بياناته. إذا شارك سياقان قاعدة بيانات، فقد دمجتهما فعلياً.

الخطأ 3: نماذج النطاق الفقيرة

وضع كل المنطق في الخدمات بينما الكيانات مجرد حاويات بيانات يهزم غرض DDD.

الخطأ 4: تجاهل اللغة المشتركة

إذا استخدم المطورون مصطلحات مختلفة عن خبراء النطاق، فإن سوء التواصل والأخطاء حتمية.

الخلاصة

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

ابدأ بالتحدث إلى خبراء النطاق. ارسم خريطة للغة التي يستخدمونها. ابحث عن حيث تتغير معاني المصطلحات. تلك هي حدودك.

مقالات ذات صلة

هندسة البرمجياتقراءة 22 دقيقة

نمط CQRS و Event Sourcing: التنفيذ مع أمثلة كود

دليل تنفيذ CQRS و Event Sourcing الكامل مع TypeScript و Node.js. يغطي مخازن الأحداث والإسقاطات واللقطات ومتى تستحق هذه الأنماط التعقيد.