ملخص المقال: حين تملك أربعة أنظمة تعريفات مختلفة للعميل ومراجع مختلفة ودورات إصدار متباعدة، تصبح الاتصالات المباشرة شبكة افتراضات لا يفهمها أحد. هذا الدليل يبني طبقة تكامل من أربع قطع: خريطة الملكية، مخزن المراجع الخارجية، محول لكل عقد، وحالة سير عمل دائمة.
طريقة القراءة: ابدأ بفهم المشكلة، ثم راجع القرار أو الخطوات العملية في الأقسام التالية، واستفد من المخطط لتصور الأجزاء المرتبطة ببعضها.
ماذا ستخرج به من هذا المقال؟
حين تملك أربعة أنظمة تعريفات مختلفة للعميل ومراجع مختلفة ودورات إصدار متباعدة، تصبح الاتصالات المباشرة شبكة افتراضات لا يفهمها أحد. هذا الدليل يبني طبقة تكامل من أربع قطع: خريطة الملكية، مخزن المراجع الخارجية، محول لكل عقد، وحالة سير عمل دائمة. سنبني الفكرة من المشكلة إلى القرار ثم إلى تطبيق يمكن نقله إلى مشروع حقيقي، مع مخطط بصري ومثال عملي وقائمة تحقق في النهاية.
حين يبدأ مشروع التكامل يكون عدد الأنظمة ثلاثة أو أربعة، وتبدو الاتصالات المباشرة بينها فكرة معقولة تمامًا. بعد سنتين يصبح لديك اثنا عشر نظامًا وأربعون اتصالًا، وكل اتصال يحمل نسخته الخاصة من معنى «العميل». هذا المقال يشرح كيف تبني طبقة تكامل تجعل هذا النمو ممكنًا: من يملك كل كيان، وكيف تُترجم العقود، وأين تُحفظ حالة سير العمل حتى يستأنف الطلب من نقطة الفشل.
اتبع الخطوات الخمس بالترتيب؛ كل واحدة تزيل نوعًا مختلفًا من الفوضى، وفي النهاية ستحصل على تصميم يمكن مراجعته قبل كتابة أي كود.
لماذا تنهار التكاملات المباشرة عند النمو؟
السبب ليس ضعف المبرمجين، بل غياب حدود واضحة. حين يستدعي كل نظام كل نظام آخر مباشرة، تتراكم أربعة أنواع محددة من المشاكل:
- انفجار الاتصالات: العدد يزيد تربيعيًا. أربعة أنظمة تحتاج ستة اتصالات، وعشرة أنظمة تحتاج خمسة وأربعين، وكل اتصال يحتاج صيانة واختبارًا مستقلين.
- تعدد المعاني:
customerIdفي نظام البطاقات رقم حساب، وفي نظام الفوترة رقم ملف. الاتصال المباشر يجعل الاثنين يبدوان شيئًا واحدًا. - تسريب نموذج المورد: يبدأ الأمر بترجمة حقلين، وينتهي بكود المجال يقرأ
CUST_STAT_CDمن نظام قديم لأنه «الأسهل». - حالة سير العمل في الذاكرة: إذا انهار الخادم بين إنشاء العميل وحفظ المستند، لا أحد يعرف أين توقف الطلب ولا ما الذي أُنشئ فعلًا.
المفاهيم الستة التي تجعل الطبقة قابلة للحياة
قبل أي تصميم، احسم هذه المفاهيم، لأن كل واحد منها يمنع فئة كاملة من الأخطاء لاحقًا:
- System of Record لكل كيان: قرار مكتوب: من يملك الحقيقة؟ الـCore يملك هوية العميل، ومخزن المستندات يملك المستندات، ولا يجوز أن يملك اثنان الحقيقة نفسها.
- Canonical Message بنية العمل: الرسالة الموحّدة تصف ما نريد فعله، لا اتحاد كل حقول الموردين. عشرون حقلًا واضحًا أفضل من ثلاثمئة حقل معظمها
null. - Adapter لكل عقد بإصدار مستقل: النظام القديم يتغير كل ثلاث سنوات وواجهة الدفع كل ثلاثة أشهر؛ الإصدار المستقل يمنع أحدهما من كسر الآخر.
- مخزن المراجع الخارجية: احفظ المعرّف الذي أعاده كل نظام في جدول دائم؛ هذه أبسط طريقة عملية لمنع إنشاء سجل مكرر عند الإعادة.
- حالة سير عمل دائمة: كل خطوة تُسجَّل مع نتيجتها ومرجعها، فيصبح «أكمل من حيث توقفت» ممكنًا بدل «أعد كل شيء».
- تنسيق مقيّد النطاق: المنسّق يملك تسلسل الخطوات فقط، ولا يملك قواعد العمل الخاصة بأي نظام.
رحلة أمر وارد عبر المحولات
الأمر الوارد لا يعرف شيئًا عن الأنظمة خلف الطبقة. المحولات تترجم إلى الأمر الموحّد، والمنسّق يحفظ الحالة ويقرر الخطوة التالية، وكل نظام يحتفظ بملكية قواعده الخاصة.
تدخل الطلبات من قناة واحدة وتُحوَّل في محولات مستقلة إلى أمر موحّد، ثم يحفظ المنسّق حالة كل خطوة ومراجعها الخارجية، بينما تبقى قواعد العمل داخل كل نظام ولا تصعد إلى الطبقة.
- قناة الطلب
- ←
- Canonical Command
- ←
- منسّق سير العمل
- ←
- محول Core Banking
- ←
- محول CRM
- ←
- محول المستندات
- ←
- مخزن الحالة والمراجع
خطوة بخطوة: ابنِ طبقة التكامل من أربع قطع
الخطوة 1: اكتب خريطة الملكية قبل أي كود
قبل أن تكتب Adapter واحدًا، اكتب من يملك ماذا. هذا الجدول يُنهي معظم الخلافات لاحقًا، لأنه يمنع وجود نسختين من الحقيقة.
# كيان واحد، مالك واحد للحقيقة، ونسخ للقراءة فقط في الباقي
entities:
customer:
owner: core-banking
replicas: [crm, analytics]
external_ref_field: core_customer_id
customer_documents:
owner: document-vault
replicas: [crm]
external_ref_field: vault_document_ref
billing_profile:
owner: billing
replicas: [crm]
external_ref_field: billing_profile_id
crm_opportunity:
owner: crm
replicas: []
external_ref_field: crm_opportunity_id
النتيجة: عند أي خلاف حول «القيمة الصحيحة» يصبح الجواب سطرًا في هذا الملف، لا نقاشًا في اجتماع.
الخطوة 2: أنشئ مخزن المراجع الخارجية
كل نظام خارجي يعيد معرّفًا خاصًا به. احفظه مرتبطًا بمفتاح العملية، وستمنع بذلك إنشاء سجل ثانٍ عند إعادة المحاولة.
CREATE TABLE integration_external_ref (
workflow_id UUID NOT NULL,
step_name TEXT NOT NULL,
system_name TEXT NOT NULL,
external_id TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (workflow_id, step_name, system_name)
);
-- يمنع إنشاء نفس الكيان مرتين في النظام نفسه
CREATE UNIQUE INDEX ux_external_ref_unique
ON integration_external_ref (system_name, external_id);
بعد هذه الخطوة يصبح سؤال «هل أنشأنا هذا العميل في الـCore سابقًا؟» استعلامًا بمفتاح أساسي، لا بحثًا نصيًا في السجلات.
الخطوة 3: اكتب محولًا واحدًا لكل عقد
المحول يعرف تفاصيل نظام واحد فقط. لا يقرأ من نظام آخر، ولا يحمل شرطًا خاصًا بنظام ثالث، ولا يكتب مباشرة في قاعدة بيانات لا يملكها.
export interface CoreBankingPort {
createCustomer(cmd: CreateCustomerCommand): Promise<{ coreCustomerId: string }>;
}
export class CoreBankingAdapter implements CoreBankingPort {
constructor(private readonly soap: CoreBankingSoapClient) {}
async createCustomer(cmd: CreateCustomerCommand): Promise<{ coreCustomerId: string }> {
// ترجمة صريحة من النموذج الموحّد إلى عقد المورد
const response = await this.soap.createCustomer({
customerType: mapCustomerType(cmd.segment), // فردي أو منشأة
nationalId: cmd.identity.value, // بلا صيغة محلية خاصة
fullNameEn: cmd.name.english,
fullNameAr: cmd.name.arabic,
segment: mapSegment(cmd.segment),
});
return { coreCustomerId: response.customerNumber }; // المرجع الخارجي
}
}
النتيجة: تغيير عقد الـCore يلمس هذا الملف وحده، ولا ينتشر إلى بقية الطبقة.
الخطوة 4: احفظ حالة سير العمل واستأنف من نقطة الفشل
لا تعتمد على الذاكرة. سجّل كل خطوة بحالتها ومرجعها، ثم اجعل الاستئناف قرارًا صريحًا: أي الخطوات تُعاد وأيها تُتخطى.
CREATE TABLE integration_workflow (
workflow_id UUID PRIMARY KEY,
workflow_type TEXT NOT NULL, -- onboarding
correlation_id UUID NOT NULL,
status TEXT NOT NULL, -- running | completed | failed
current_step TEXT NOT NULL,
attempt_count INT NOT NULL DEFAULT 0,
payload_hash TEXT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE integration_step (
workflow_id UUID NOT NULL REFERENCES integration_workflow(workflow_id),
step_name TEXT NOT NULL,
status TEXT NOT NULL, -- pending | done | failed | skipped
error_code TEXT,
finished_at TIMESTAMPTZ,
PRIMARY KEY (workflow_id, step_name)
);
مع هذا الجدول يصبح سؤال «أين توقف طلب الأمس؟» استعلامًا واحدًا، ويصبح الاستئناف آمنًا لأن الخطوات المكتملة تحمل مراجعها المحفوظة.
الخطوة 5: ألزم كل استدعاء بـCorrelation وبيانات تدقيق
Correlation ID واحد يمر عبر كل خطوة ومع كل طلب خارجي، ويُكتب في السجل قبل الإرسال لا بعده، وإلا فقدت أثر الطلب الذي انتهت مهلته.
export async function runOnboarding(cmd: CreateCustomerCommand) {
const workflowId = randomUUID();
await state.start(workflowId, 'onboarding', cmd.correlationId);
// الخطوة 1: الـCore هو مالك هوية العميل
const core = await resumeOrRun(workflowId, 'core_customer', async () => {
const ref = await coreAdapter.createCustomer(cmd);
await refs.save(workflowId, 'core_customer', 'core-banking', ref.coreCustomerId);
return ref;
});
// الخطوة 2: CRM يقرأ فقط، ويستقبل المرجع الخارجي صريحًا
await resumeOrRun(workflowId, 'crm_profile', () =>
crmAdapter.upsertProfile({ ...cmd, coreCustomerId: core.coreCustomerId }),
);
// الخطوة 3: المستندات تُخزَّن بمرجع لا بجسم الملف
await resumeOrRun(workflowId, 'document_vault', () =>
documentAdapter.store({ ownerRef: core.coreCustomerId, documentRef: cmd.documentRef }),
);
await state.complete(workflowId);
}
النتيجة: إذا فشل CRM بعد نجاح الـCore، فإن إعادة تشغيل الأمر لا تُنشئ عميلًا ثانيًا، لأن core_customer مسجّلة كخطوة منتهية مع مرجعها.
مثال عملي كامل: إنشاء عميل عبر Core وCRM ومخزن مستندات
سيناريو تطبيقي
تصل عملية Onboarding واحدة بثلاث خطوات. نجحت خطوة الـCore وأنشأت العميل 4410299، ثم انتهت مهلة CRM بعد 2.5 ثانية. لأن الطبقة حفظت الحالة والمرجع الخارجي قبل الاستدعاء، لم يُعَد إنشاء العميل؛ استأنف المنسّق من خطوة CRM وحدها بعد 10 ثوانٍ، ثم خزّن المستند بمرجع العميل نفسه. لو كان المرجع في الذاكرة فقط لكانت إعادة المحاولة أنشأت عميلين بالرقم الوطني ذاته، وهي حالة تُصحَّح يدويًا وتستهلك ساعات من فريق العمليات.
export async function resumeOrRun<T>(
workflowId: string,
step: string,
run: () => Promise<T>,
): Promise<T> {
const existing = await state.getStep(workflowId, step);
if (existing?.status === 'done') return existing.result as T; // لا تُعِد تنفيذ ما نجح
try {
const result = await run();
await state.markDone(workflowId, step, result);
return result;
} catch (error) {
await state.markFailed(workflowId, step, toErrorCode(error));
throw error; // يُستأنف لاحقًا
}
}
مقارنة: التنسيق المركزي مقابل التنسيق الموزّع
من يقود سير العمل بين الأنظمة؟
Orchestration
- مكان واحد يمكن تتبعه عند فشل خطوة
- مناسب للعمليات المالية التي تحتاج ترتيبًا صارمًا
- خطر: يتحول تدريجيًا إلى Monolith جديد
- يحتاج فصلًا صارمًا بين التنسيق وقواعد العمل
Choreography
- الأنظمة تتفاعل عبر أحداث بلا قائد مركزي
- مرونة أعلى عند إضافة مستهلك جديد
- تتبّع الفشل أصعب ويحتاج Correlation قويًا
- يصعب فيه ضمان ترتيب الخطوات الحساسة
القاعدة العملية: استخدم Orchestration عندما يمتد سير عمل واحد عبر أنظمة وتحتاج معرفة أين توقف الطلب. واستخدم الأحداث عندما تكون الخطوات مستقلة ولا تحتاج ترتيبًا مضمونًا.
أخطاء شائعة
أخطاء تجعل الطبقة عبئًا لا حلًا
- بناء Universal DTO بمئات الحقول القابلة لـnull من كل الموردين.
- السماح للمحول بالكتابة مباشرة في قاعدة بيانات نظام لا يملكه.
- حفظ حالة سير العمل في الذاكرة فقط ثم إعادة تشغيل الخادم.
- تحويل كل الأخطاء إلى integration failed بلا رمز مصدر يوضح أي نظام فشل.
- تكرار منطق الترجمة نفسه في كل قناة تستهلك الطبقة.
- ترك سؤال «من يملك هذا الحقل؟» بلا جواب مكتوب.
قائمة تحقق سريعة
- اكتب خريطة System of Record لكل كيان قبل أول Adapter.
- احفظ المراجع الخارجية في جدول دائم مع قيد فريد عليها.
- أبقِ المحول مسؤولًا عن عقد خارجي واحد فقط.
- سجّل حالة كل خطوة قبل تنفيذها لتُصبح الاستئناف آمنًا.
- مرّر Correlation ID واحدًا عبر كل خطوة وكل طلب خارجي.
- لا تدع المنسّق يحتوي قواعد عمل تخص نظامًا معينًا.
- اختبر الفشل بين كل خطوتين، لا المسار السعيد فقط.
الأسئلة الشائعة
كم نظامًا يحتاج طبقة تكامل فعلًا؟
العبرة ليست بالعدد بل بعدد الاتصالات وتكرار منطق الترجمة. نظامان يحتاجان اتصالًا واحدًا، وهناك تصبح الطبقة ترفًا. لكن حين تصل إلى أربعة أنظمة تصبح الاتصالات المباشرة اثني عشر اتصالًا، وكل واحد منها نسخة جديدة من الترجمة. عند هذه النقطة تدفع الطبقة ثمنها من أول تغيير.
هل يجب أن يملك المنسّق قواعد العمل؟
لا. المنسّق يملك تسلسل الخطوات والاستئناف ومعرفة أين توقف الطلب، ولا يملك شروطًا مثل «هل يحق لهذا العميل فتح حساب؟». القاعدة تبقى في النظام الذي يملك الكيان، وإلا تحولت الطبقة إلى نظام مركزي جديد يحمل قواعد الجميع.
كيف أتعامل مع نظام خارجي لا يدعم Idempotency؟
احفظ مرجعك الخارجي في جدولك الخاص قبل الاستدعاء، واستعلم عن الوجود عند الإعادة بدل الإنشاء. إن لم يوفر النظام استعلامًا، فاستخدم مفتاحًا حتميًا مبنيًا على بيانات ثابتة مثل الرقم الوطني، ووثّق أن المطابقة تتم من جانبك لا من جانبه.
الخلاصة
ما يجب أن يبقى معك
- كل كيان له مالك واحد للحقيقة، والباقي نسخ للقراءة.
- الرسالة الموحّدة تصف نية العمل لا اتحاد حقول الموردين.
- محول واحد لكل عقد خارجي بإصدار مستقل.
- المراجع الخارجية وحالة الخطوات تُخزَّن قبل الاستدعاء لتُصبح الاستئناف آمنًا.
- المنسّق يقود التسلسل ولا يملك قواعد العمل.
الخطوة التالية
بعد أن أصبح لكل نظام حد واضح داخل الطبقة، يبقى سؤال الحافة: هل تحتاج بوابة API أمام هذه الخدمات، أم أنها ستضيف تعقيدًا بلا مقابل؟ المقال التالي يعطيك معايير قرار محددة للإجابة.
كيف تستخدم هذا المقال في عمل حقيقي؟
لا تنقل الحل حرفيًا. ابدأ بتحديد السياق والقيود في نظامك، ثم اختر الجزء الذي ينطبق عليك، طبّقه على نطاق صغير، وقِس النتيجة قبل التوسع. إذا كان القرار يؤثر على الأمان أو البيانات أو التكاملات، دوّن الافتراضات وحدود المسؤولية بوضوح حتى يستطيع شخص آخر مراجعتها.
مخطط يربط عناصر الموضوع
flowchart LR
A["طلب العميل"]
B["مصادقة وتوثيق"]
C["طبقة التكامل"]
D["الخدمة الخارجية"]
E["التحقق والتدقيق"]
A --> B
B --> C
C --> D
D --> E
التسلسل: طلب العميل ← مصادقة وتوثيق ← طبقة التكامل ← الخدمة الخارجية ← التحقق والتدقيق.
سيناريو تطبيقي ومراجعة
افترض أنك مسؤول عن تطبيق ما ورد في هذا المقال داخل فريق عمل. ابدأ بتحديد الهدف الذي يقيس النجاح في موضوع كيف تصمم API Integration Layer بين أنظمة مختلفة؟، ثم اكتب المدخلات المطلوبة والقيود والصلاحيات، وجرب حالة طبيعية وأخرى استثنائية قبل الاعتماد.
- حدد الافتراضات التي تحتاج إلى تأكيد في بيئتك.
- حوّل القرارات الأساسية الواردة أعلاه إلى خطوات أو اختبارات قابلة للتنفيذ.
- وثّق طريقة التراجع أو التصحيح إذا فشل أحد الاختبارات.
أسئلة للمراجعة
كيف أبدأ بتطبيق هذا الموضوع؟
اختر حالة استخدام واحدة، وطبّق أصغر جزء يحقق قيمة قابلة للقياس؛ ثم قارن النتيجة بالمتطلبات.
كيف أتحقق من جودة الحل؟
اختبر المسار المعتاد ومسار الخطأ، وراجع الأمان وسلامة البيانات والأداء وفق طبيعة الموضوع، ثم أعد الاختبار بعد أي تعديل.
الرد على التعليق