لماذا CQRS و Event Sourcing ليسا "إفراطًا في الهندسة" بل إنقاذًا للأنظمة المعقدة
\n\nهل سبق لك أن واجهت موقفًا حيث يتعامل نظامك المكتوب بلغة Rust بشكل مثالي مع طلبات القراءة، ولكنه "ينهار على ركبتيه" عند الكتابة؟ أو العكس - تضحي بأداء القراءة من أجل القدرة على حفظ السجل الكامل للتغييرات؟ هل يبدو ذلك مألوفًا؟ إذن مرحبًا بك في عالم CQRS و Event Sourcing.
\n\nيعتبر العديد من المطورين هذه الأنماط "مدفعية ثقيلة"، قابلة للتطبيق فقط في مشاريع المؤسسات بلغة C# أو Java. ولكن مع تزايد شعبية Rust في الأنظمة عالية التحميل (من خوادم الألعاب إلى المنصات المالية)، أصبحت CQRS و Event Sourcing ليست مفيدة فحسب - بل أصبحت ضرورية. Rust مع تكلفتها الصفرية للتجريد والكتابة الصارمة هي اللغة المثالية لتنفيذ هذه الأنماط.
\n\nفي هذه المقالة، لن نتحدث عن النظرية فقط. سنكتب كودًا عمليًا بلغة Rust يمكنك تكييفه مع مشاريعك. ستتعلم كيفية فصل المسؤولية بين الأوامر والاستعلامات، وكيفية تخزين الأحداث بدلاً من الحالة الحالية، وكيف يسرع كل ذلك نظامك أضعافًا مضاعفة.
\n\nما هما CQRS و Event Sourcing؟ بلغة بسيطة لمطور Rust
\n\nتخيل أنك تدير حسابًا بنكيًا. في نموذج CRUD التقليدي، لديك جدول واحد "حسابات" حيث يتم تخزين الرصيد الحالي. إذا كنت بحاجة إلى عرض السجل، فستقوم بإنشاء جدول آخر "المعاملات". ولكن ماذا لو كنت لا تحتاج فقط إلى معرفة الرصيد، بل إلى فهم لماذا تغير؟ أو استعادة الحالة في أي تاريخ؟
\n\nCQRS: فرق تسد
\n\nCQRS (Command Query Responsibility Segregation) هو مبدأ يفصل العمليات إلى مجموعتين:
\n- \n
- الأوامر (Commands) - تغير الحالة. لا تعيد بيانات، فقط تأكيدًا أو خطأ. \n
- الاستعلامات (Queries) - تقرأ البيانات فقط. لا تغير الحالة. \n
في Rust، يبدو هذا الفصل طبيعيًا: أنواع الأوامر والاستعلامات هي هياكل مختلفة، ومعالجاتها هي دوال أو وحدات مختلفة. هذه ليست مجرد "ممارسة جيدة"، بل هي طريقة لتجنب التغييرات العشوائية أثناء القراءة.
\n\n// الأمر - يغير الحالة\npub struct CreateOrder {\n pub user_id: Uuid,\n pub items: Vec<OrderItem>،\n}\n\n// الاستعلام - يقرأ فقط\npub struct GetOrderHistory {\n pub user_id: Uuid,\n pub limit: usize،\n}\n\nEvent Sourcing: احفظ السجل، وليس لقطة لحظية
\n\nEvent Sourcing هو نهج حيث لا نحفظ الحالة الحالية للكائن، بل تسلسل الأحداث التي أدت إليها. هل تريد معرفة رصيد الحساب في 1 يناير 2023؟ فقط "شغّل" جميع الأحداث من البداية حتى ذلك التاريخ.
\n\nالمزايا واضحة:
\n- \n
- التدقيق - كل تغيير مسجل. \n
- اللقطات الزمنية - يمكن استعادة أي حالة. \n
- مقاومة الأخطاء - إذا كان هناك خطأ في الكود، لم تفقد البيانات، بل قمت فقط "بتشغيلها" بشكل غير صحيح. \n
مثال عملي: نظام إدارة الطلبات بلغة Rust
\n\nدعونا نبني نظامًا مصغرًا لمتجر إلكتروني. سيعالج الأوامر (إنشاء طلب، إضافة منتج) والاستعلامات (الحصول على سلة التسوق، سجل الطلبات). كل المنطق سيكون مبنيًا على الأحداث.
\n\nالخطوة 1: تحديد الأحداث والأوامر
\n\nأول ما يجب فعله هو تصميم المجال. في Rust، نستخدم التعدادات (enum) للأحداث والهياكل الصارمة للأوامر.
use serde::{Deserialize, Serialize};\nuse uuid::Uuid;\n\n// الأحداث - حقائق غير قابلة للتغيير\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub enum OrderEvent {\n Created { order_id: Uuid, user_id: Uuid, timestamp: i64 },\n ItemAdded { order_id: Uuid, item: Str