يا جماعة الخير، السلام عليكم ورحمة الله.
اسمحولي أبدأ بقصة صغيرة صارت معي قبل كم سنة. كنا شغالين على نظام كبير لشركة تجارة إلكترونية، نظام فيه كل إشي: إدارة للمنتجات، طلبات، دفع، شحن، خدمة عملاء… كل خدمة (service) كانت عبارة عن تطبيق صغير لحالها، والمفروض إنها “مايكروسيرفيس” يعني. بس يا ويلي شو كانت “مايكروسيرفيس”!
بتذكر مرة، فريق التسويق طلب تعديل بسيط: “بدنا لما الزبون يسجل حساب جديد، نبعتله إيميل ترحيب مع كود خصم”. قلنا بسيطة، شغلة نص ساعة. دخل المبرمج المسؤول عن خدمة التسجيل (User Service) وأضاف سطرين كود عشان ينادي خدمة الإشعارات (Notification Service). رفعنا التحديث، وكل شي تمام… لمدة يومين.
بعد يومين، صار في ضغط على سيرفر الإيميلات، وخدمة الإشعارات صارت بطيئة شوي. وهون بلّشت المصايب. صارت عملية تسجيل المستخدم الجديد تعلق! المستخدم يحط معلوماته، يكبس “تسجيل”، والصفحة تضلها تحمل… تحمل… لحد ما تطلعله رسالة خطأ. الدنيا قامت وقعدت، والزبائن بشتكوا، والإدارة فوق راسنا. ليش؟ لأنه خدمة التسجيل كانت مرتبطة “بالحبل السري” مع خدمة الإشعارات. إذا خدمة الإشعارات عطست، خدمة التسجيل بتصيبها إنفلونزا.
وقتها قعدت مع حالي، صفنت وحكيت: “يا أبو عمر، هيك ما بنفع نكمل. خدماتنا مش بتتكلم مع بعض، خدماتنا بتصرّخ على بعض! أي وحدة بتوقع بتسحب الباقي معها”. هذا الأخطبوط اللي اسمه “الاقتران الخانق” كان رح يغرق سفينتنا. ومن هون بلشت رحلتنا للبحث عن حل، والحل كان اسمه: المعمارية الموجهة بالأحداث (Event-Driven Architecture).
ما هو جحيم “الاقتران الخانق” (Tight Coupling)؟
قبل ما نحكي عن الحل، خلينا نفهم المشكلة منيح. تخيل إنك بتبني حيط من الحجر، بس بدل ما تستخدم مونة (إسمنت) بين الحجارة، قررت تلحمهم ببعض بالحديد. الحيط رح يكون قوي مبدئياً، بس لو بدك تغير حجر واحد بس؟ رح تضطر تكسر اللحام، وممكن تكسر الحجارة اللي جنبه. هذا هو بالضبط الاقتران الخانق في البرمجة.
لما تكون خدمة (أ) لازم تنادي خدمة (ب) مباشرة عشان تكمل شغلها، بنسمي هاي العلاقة “اقتران متزامن” (Synchronous Coupling). وهذا بيخلق عدة مشاكل كارثية:
- نقطة فشل مركزية (Single Point of Failure): إذا وقعت خدمة (ب)، خدمة (أ) رح تفشل معها، وممكن خدمة (ج) اللي بتعتمد على (أ) تفشل كمان، وهكذا دواليك. بتصير زي أحجار الدومينو.
- صعوبة التطوير: أي تغيير في خدمة (ب) (مثلاً تغيير في الـ API)، بيتطلب تغيير وتحديث في كل الخدمات اللي بتناديها. بتصير عملية التطوير بطيئة ومعقدة.
- انعدام المرونة: صعب جداً تضيف خدمة جديدة (مثلاً خدمة “د”) لازم تستجيب لنفس الحدث بدون ما تعدل على الخدمة الأصلية (أ).
باختصار، الاقتران الخانق بحوّل نظامك من مجموعة خدمات مستقلة إلى كتلة واحدة متصلبة، أي تغيير فيها بصير زي عملية جراحية خطيرة.
طوق النجاة: المعمارية الموجهة بالأحداث (EDA)
طيب يا أبو عمر، شو الحل؟ الحل هو نغير طريقة تفكيرنا تماماً. بدل ما الخدمات تنادي بعضها مباشرة، رح نخليها تتواصل بطريقة غير مباشرة، مثل الصدى في وادٍ واسع. هذا هو جوهر الـ EDA.
في الـ EDA، الخدمة ما بتطلب شي من خدمة ثانية. هي فقط بتعلن عن “حدث” (Event) صار عندها. مثلاً، خدمة المستخدمين ما بتنادي خدمة الإشعارات، هي بس بتعلن للعالم كله: “يا عالم، لقد تم تسجيل مستخدم جديد! وهذا هو رقمه: 123”.
هذا الإعلان (الحدث) ما بروح بالهوا، بروح لمكان مركزي بنسميه “وسيط الأحداث” أو “ناقل الأحداث” (Event Broker / Message Bus). والخدمات الثانية اللي بهمها هالموضوع (مثل خدمة الإشعارات، خدمة التحليلات، خدمة CRM) بتكون “مشتركة” (Subscribed) في هذا النوع من الأحداث. لما وسيط الأحداث يستقبل الحدث، بوزعه على كل المشتركين المهتمين.
لاحظ الفرق الجوهري: خدمة المستخدمين (المنتج للحدث) ما بتعرف أصلاً مين رح يستقبل الحدث، ولا بهمها. هي عملت اللي عليها ورمت الخبر في الباص. وخدمة الإشعارات (المستهلك للحدث) ما بتعرف مين أنتج الحدث، هي بس بتعرف إنه وصلها حدث من النوع اللي بهمها ولازم تتعامل معه.
المكونات الأساسية للـ EDA
- الحدث (Event): رسالة صغيرة بتوصف شي صار في الماضي. مثلاً:
OrderPlaced,UserRegistered,PaymentFailed. الحدث لازم يحتوي على كل المعلومات اللازمة للمستهلكين عشان يقوموا بعملهم. - منتج الحدث (Event Producer): هو أي خدمة بتصدر أو بتنشر حدث معين. في مثالنا، خدمة الطلبات (Order Service) هي منتج لحدث
OrderPlaced. - مستهلك الحدث (Event Consumer): هو أي خدمة بتستمع أو بتشترك في نوع معين من الأحداث وبتتفاعل معه. في مثالنا، خدمة الإشعارات وخدمة المخزون هم مستهلكين لحدث
OrderPlaced. - وسيط الأحداث (Event Broker): هو القلب النابض للنظام. هو المسؤول عن استلام الأحداث من المنتجين وتوصيلها بشكل موثوق لكل المستهلكين المهتمين. أشهر الأمثلة عليه: RabbitMQ, Apache Kafka, Google Pub/Sub, AWS SNS/SQS.
مثال عملي: من الصراخ إلى الهمس
خلينا نرجع لمثال التجارة الإلكترونية ونشوف كيف الـ EDA بتحل المشكلة.
السيناريو القديم (الاقتران الخانق):
- الزبون يضغط “تأكيد الطلب” في الواجهة الأمامية.
- الواجهة تنادي الـ API تبع خدمة الطلبات (Order Service).
- خدمة الطلبات تحفظ الطلب في قاعدة البيانات.
- خدمة الطلبات تنادي مباشرة خدمة المخزون (Inventory Service) لخصم الكمية. (انتظار…)
- خدمة المخزون ترد بنجاح.
- خدمة الطلبات تنادي مباشرة خدمة الإشعارات (Notification Service) لإرسال إيميل. (انتظار…)
- خدمة الإشعارات ترد بنجاح.
- خدمة الطلبات ترد للواجهة الأمامية بأن الطلب تم بنجاح.
المشكلة: لو خدمة الإشعارات بطيئة أو واقعة، كل العملية بتعلق عند خطوة رقم 6، والزبون رح يشوف شاشة تحميل لا نهائية، وممكن الطلب يفشل كله.
السيناريو الجديد (معمارية الأحداث):
- الزبون يضغط “تأكيد الطلب”.
- الواجهة تنادي الـ API تبع خدمة الطلبات (Order Service).
- خدمة الطلبات تحفظ الطلب في قاعدة البيانات.
- خدمة الطلبات تنشر حدث اسمه
OrderPlacedEventإلى وسيط الأحداث (مثلاً RabbitMQ). الحدث يحتوي على كل تفاصيل الطلب. - خدمة الطلبات ترد فوراً للواجهة الأمامية بأن الطلب تم استلامه بنجاح. (العملية سريعة جداً!)
الآن، في الخلفية وبشكل غير متزامن (Asynchronously):
- وسيط الأحداث يرى أن خدمة المخزون وخدمة الإشعارات مشتركتان في هذا الحدث.
- يرسل نسخة من حدث
OrderPlacedEventإلى خدمة المخزون، التي تقوم بخصم الكمية. - يرسل نسخة أخرى من نفس الحدث إلى خدمة الإشعارات، التي تقوم بإرسال الإيميل.
شفتوا الجمال؟ خدمة الطلبات خلصت شغلها بسرعة البرق. ما بتعرف أصلاً بوجود خدمة اسمها “إشعارات” أو “مخزون”. لو خدمة الإشعارات واقعة لمدة ساعة؟ ولا يهم، وسيط الأحداث رح يحتفظ بالرسالة لحد ما خدمة الإشعارات ترجع تشتغل وياخدها. النظام صار مرن وقوي وقادر على تحمل الأخطاء.
شوية كود للتوضيح (Pseudo-code)
هيك ممكن يكون شكل الكود في خدمة الطلبات (المنتج):
// Using a hypothetical library for an event broker
class OrderService {
constructor(private eventBroker) {}
placeOrder(orderDetails) {
// 1. Save order to the database
const newOrder = db.saveOrder(orderDetails);
// 2. Create the event payload
const event = {
name: 'OrderPlacedEvent',
payload: {
orderId: newOrder.id,
customerId: newOrder.customerId,
items: newOrder.items,
timestamp: new Date()
}
};
// 3. Publish the event and forget about it
this.eventBroker.publish('orders.placed', event);
// 4. Return success to the user immediately
return { success: true, orderId: newOrder.id };
}
}
وهيك ممكن يكون شكل الكود في خدمة الإشعارات (المستهلك):
// Using a hypothetical library for an event broker
class NotificationService {
constructor(private eventBroker) {
// Subscribe to the event when the service starts
this.listenForOrderPlacedEvents();
}
listenForOrderPlacedEvents() {
this.eventBroker.subscribe('orders.placed', (event) => {
console.log(`Received OrderPlacedEvent for order: ${event.payload.orderId}`);
// Send the confirmation email
this.sendConfirmationEmail(event.payload.customerId, event.payload.orderId);
});
}
sendConfirmationEmail(customerId, orderId) {
// ... logic to find customer email and send it
console.log(`Email sent to customer ${customerId} for order ${orderId}.`);
}
}
الكود مجرد للتوضيح، لكن الفكرة واضحة. فصل تام بين المنتج والمستهلك.
نصائح من خبرة أبو عمر
الانتقال للـ EDA مش بكبسة زر، وفي شوية مطبات تعلمنا منها بالطريقة الصعبة. خذوا هالنصايح من أخوكم:
- ابدأ صغيرًا (Start Small): ما في داعي تعيد كتابة كل نظامك مرة وحدة. “شوي شوي يا حبيبي”. اختار منطقة واحدة في نظامك بتعاني من مشاكل الاقتران وابدأ طبق فيها الـ EDA. مثلاً، ابدأ بميزة جديدة أو خدمة واحدة.
- المراقبة ثم المراقبة (Observability): في الـ EDA، تتبع مسار عملية كاملة (مثلاً من الطلب للشحن) بصير أصعب لأنه كل شي غير متزامن. لازم تستثمر في أدوات التتبع الموزع (Distributed Tracing) مثل Jaeger أو OpenTelemetry عشان تعرف كل حدث وين راح ومين استلمه وشو عمل فيه. “بدك تعرف وين راحت الرسالة!”.
- تعامل مع الفشل بحكمة: شو بصير لو خدمة الإشعارات حاولت تعالج حدث وفشلت (مثلاً بسبب خطأ في الكود)؟ لو ما عملت إشي، وسيط الأحداث رح يضل يحاول يبعتلها نفس الرسالة للأبد. هون بيجي دور مفهوم الـ “طابور الموتى” (Dead-Letter Queue – DLQ). أي رسالة بتفشل معالجتها عدد معين من المرات، بتنتقل تلقائياً للـ DLQ عشان المطورين يشوفوها ويحللوا المشكلة لاحقاً بدون ما توقف النظام كله.
- اتفقوا على شكل الأحداث (Schema & Versioning): الأحداث هي العقد (Contract) الجديد بين خدماتك. لازم يكون في اتفاق واضح على شكل كل حدث (Schema). استخدموا أدوات مثل JSON Schema أو Avro. وفكروا من أول يوم كيف رح تطوروا هاي الأحداث في المستقبل (Versioning) بدون ما تكسروا الخدمات القديمة.
الخلاصة
المعمارية الموجهة بالأحداث (EDA) ما كانت مجرد “تقنية جديدة” طبقناها، بل كانت تغيير في فلسفة تصميم البرمجيات كلها. نقلتنا من عالم الأنظمة الهشة والمتصلبة إلى عالم الأنظمة المرنة، القوية، والقابلة للتوسع بشكل لا يصدق. صرنا نقدر نضيف خدمات جديدة تستمع لنفس الأحداث بدون ما نلمس أي سطر كود في الخدمات القديمة. صار كل فريق بقدر يشتغل على خدمته ويطورها باستقلالية شبه تامة.
هل هي الحل لكل المشاكل؟ طبعاً لا. الـ EDA بتضيف طبقة من التعقيد (وسيط الأحداث) ولازم تفهمها منيح عشان ما تسبب مشاكل جديدة. لكن بالنسبة للأنظمة الكبيرة والمعقدة اللي بتحتاج تكون مرنة ومتاحة دايماً، هي مش مجرد خيار، بل ضرورة.
نصيحتي الأخيرة لكل مطور أو مهندس برمجيات: لا تخاف من تفكيك الروابط الخانقة. اسمح لخدماتك أن تتنفس وتتحدث بهدوء واستقلالية. صدقني، نومك في الليل رح يصير أهدأ بكثير 😉.