خريطة Domain
يبدأ التعرّف على نظام جديد دائمًا بسؤال: لماذا وُجد وماذا يفعل. هذه أسئلة عن المعنى، لا عن البنية. لكن الإجابة الأولى تكون عادةً بنية تحتية: متحكّمات، وخدمات، ومستودعات، وترحيلات. ومجال المشكلة لا يكاد يُرى من خلفها.
خريطة Domain هي الإجابة عن السؤال الأول. لا يوجد عليها سوى المجالات التي يتكوّن منها النظام: StoreDomain — إدارة الطلبات، BillingDomain — الفوترة، MessagingDomain — الإشعارات. إجابة بلا تفاصيل تغطّي النظام كاملًا وتتّسع لها الذاكرة — وهي نقطة الارتكاز الأولى.
StoreDomainإدارة الطلبات
BillingDomainالفوترة
MessagingDomainالإشعارات
الكيانات تُعبِّر عن المجال
في الـDomain شيئان رئيسيان: Entity — الطلب والعميل والبند — وAction، وهي العمليات المسمّاة التي تجري عليها. لا متحكّمات ولا جداول ولا بقية البنية التحتية. وهذا يتيح النزول خطوة أعمق مع إبقاء التركيز على مقصد النظام في العمل، لا على تنفيذه التقني.
الـEntity صنف عادي: اسم وحقول محدَّدة النوع ولا شيء غير ذلك. لا يعرف شيئًا عن ORM ولا عن الجداول ولا عن المكان الذي ستأتي منه البيانات — مهمته الوحيدة أن يصف كائنًا من العالم الحقيقي كما يُتحدَّث عنه: للطلب مبلغ وعملة، وللعميل اسم وبريد. والحقول نوعان: بسيطة — نص ورقم وتاريخ — وإشارات إلى Entity أخرى، لأن للطلب عميلًا وله بنود. والنوع الثاني هو ما يحوّل مجموعة أصناف منفصلة إلى نموذج، وعنه يدور القسم التالي. أما السلوك فغير موجود: الـEntity يصف فقط، وكل ما يُفعل بالكائن يعيش في Action.
ما هو الـAction →العلاقات تُحدِّد الملكية صراحةً
بات واضحًا مِمَّ يتكوّن الـDomain. لكن ما الذي يمسكه معًا، وما الذي يمنع علاقاته من أن تتنافر؟
تُوصَف العلاقة بأمرين: مدى تشبّث الكائنين ببعضهما — تركيب أو تجميع أو ارتباط — وكم كائنًا يقف على كل جانب، واحد أم عدة. وكل كيان يعلن العلاقة كاملة: جانبه والجانب المقابل، بنوعٍ وكثرةٍ لكليهما. لذلك يكفي النظر إلى كيان واحد لترى جميع علاقاته دفعةً واحدة. وعند الإقلاع يقابل النظام كل زوج — فإن اختلف الجانبان توقّف التشغيل. وأحيانًا لا يوجد جانب ثانٍ: العلاقة تنظر إلى ما وراء مخزنها. وهذا ممكن أيضًا، لكن يُعلَن صراحة.
الـLifecycle يتحكم في كل انتقال
ما دامت الحالة مجرّد نص، فلا شيء يمنع طلبًا من القفز من المسودة إلى التسليم مباشرة. وما هو مسموح وما هو ممنوع يعيش في رؤوس الفريق، أو في تعليق تجاوزه الزمن منذ وقت طويل.
في AOA تكفّ هذه القواعد عن أن تكون شفهية: تُعلَن حالات الطلب والانتقالات بينها إلى جوار الكيان نفسه، بوصفها آلة حالات — مسودة، مدفوع، مشحون، مُسلَّم — والإلغاء ممكن من الحالتين الأوليين فقط. وليس هذا توصية: الانتقال غير المدرَج غير موجود، ولا يمكن للطلب أن يصير مُسلَّمًا دون أن يكون قد دُفع. والآلة نفسها تُفحَص عند الإقلاع: حالة لا يمكن بلوغها، أو انتقال إلى لا شيء، يوقفان التشغيل قبل مرور أول طلب حقيقي بزمن طويل.
تُعلَن الآلة وحدها: أي حالات يمكن أن يتّخذها الطلب، وأي انتقالات بينها مسموحة. مرّة واحدة، بمعزل عن أي كيان.
والآن يمكن تطبيقها: في OrderEntity يكفّ الحقل status عن كونه نصًّا ويصير من نوع OrderLifecycle. النص كان يقبل أي قيمة؛ وهذا النوع لا يقبل إلا ما إليه طريق.
الـResources تُحدِّد حد التخزين
يصف النموذج مجال المشكلة، لكنه لا يتعامل مع قواعد البيانات مباشرةً: يجب ألّا يعتمد على كيفية بنائها. إذن لا بدّ لشيءٍ آخر أن يجلب البيانات من قاعدة حقيقية — وأن يعيدها.
هذا هو عمل الـResource. فهو يحمل كل ما يجب أن يعيش بين الاستدعاءات: الاتصال والمجمّع والعميل. ومهمته أن يفتح ويُنفِّذ ويُعيد؛ ولا قواعد عمل بداخله. والذي يعبر هذا الحدّ هو الكيانات. والنموذج الواحد نفسه يمكن أن يُمثَّل في عدّة Resource في آنٍ واحد: أحدها يقرأ من SQL، وآخر من NoSQL، وثالث من خدمة HTTP تخصّ غيرك، وكلها تُعيد الـOrderEntity نفسه. ولا يعرف كود منطق العمل أيّها الذي اشتغل: لا اسم جدول ولا لهجة ولا صيغة استجابة يصل إليه. ولذلك فتغيير المخزن يعني تغيير الـResource وحده.
الإسقاطات تُشكِّل الشريحة المطلوبة
يُقرأ الكيان نفسه من قاعدة البيانات تارةً كاملًا، وتارةً جزئيًا، وتارةً بمعرّفٍ وحده. والجواب المعتاد على ذلك حديقةُ DTO، صنفٌ مستقلٌّ لكل حالة.
الأسلوب المعتاد أن يُصنَع DTO خاص لكل دالة من دوال الـResource. وهو، خلافًا لإرجاع القواميس العادية، يمنح حمايةً ساكنة من الأخطاء المطبعية في أسماء الحقول. لكن للأسلوب وجهًا آخر: يتفتّت الكيان الأصلي إلى حشد من الـDTO الصغيرة، ولا يبقى نموذج المجال إلا في التوثيق وفي مخططات تتقادم قبل أول إصدار.
وفكرة AOA ألّا يُتخلّى عن النموذج الكامل: فدالة الـResource تُعيد كائنات النموذج نفسها، كما أُعلنت تمامًا. وتُقرأ الحقول عبر سمات مسمّاة كما في الـDTO، دون أن تظهر أصناف جديدة — ويغدو مستحيلًا أن يُوضع في النموذج حقلٌ لا وجود له فيه. وحداثته يؤكّدها الكود نفسه الذي يستعمله، ومخطط ERD يُرسم منه مباشرةً.
ولذلك يُطرَح مفهوم إسقاط البيانات: حين تقرأ دالة الـResource الكيان قراءةً جزئية، فهي تُعيد الكيان نفسه، لكن الحقول التي لم تُقرأ من قاعدة البيانات تُوسَم بأنها غير متاحة. وعند أول محاولة لاستعمالها يرفع النظام استثناءً واضحًا بدل أن يصمت.
نموذج ERD في Maxitor →get_order(order_id) → OrderEntity
الحالة الواضحة — الكيان بأكمله، كل حقل محمَّل؛ صنف النموذج مُستخدَم تمامًا كما أُعلِن.
find_orders(query) → list[OrderEntity]
مجموعة رقيقة — لا يُحمَّل سوى id كل طلب؛ وقراءة أي حقل آخر كانت ستُطلِق خطأً.
get_full_order_info(order_id) → OrderEntity
الحالة العميقة — الطلب إضافةً إلى كياناته المتداخلة، كل واحد منها محمَّل إلى عمقه الجزئي الخاص (id يُحمَّل دائمًا، فهو المفتاح).
