الهندسة السداسية في C++: كيف تتوقف عن الخوف من التغييرات وتحب الاختبارات
\n\nهل سبق لك أن أعدت كتابة وحدة عمل مع قاعدة بيانات، ثم أدركت أنك بحاجة لتغيير نصف منطق الأعمال؟ أو حاولت تغطية فصل دراسي باختبارات الوحدة، وهو يسحب معه ثلاثة أطنان من التبعيات من العالم الخارجي؟ إذا كان الأمر كذلك، فمرحبًا بك في النادي. لحسن الحظ، لدينا أداة تحل هذه المشاكل مرة واحدة وإلى الأبد: الهندسة السداسية (والمعروفة أيضًا بهندسة Ports & Adapters).
\n\nفي هذه المقالة، سنشرح كيفية تطبيق هذا المفهوم القوي في C++. سترى ليس فقط مخططات مجردة، بل كودًا حقيقيًا يمكنك استخدامه الآن. ونعم، سنفعل ذلك دون تعصب - بحيث لا ينفجر المشروع القديم، ويكون الكود الجديد نظيفًا.
\n\nما هي الهندسة السداسية (وما علاقة الشكل السداسي بها)؟
\n\nتخيل نواة تطبيقك - وهي منطق الأعمال الخالص الذي لا يعرف شيئًا عن قواعد البيانات، ولا عن REST API، ولا عن الواجهة الرسومية. حول هذه النواة توجد المنافذ (الواجهات)، وأبعد من ذلك توجد المحولات (التطبيقات الملموسة). بصريًا، يشبه هذا الشكل السداسي (hexagon)، ومن هنا جاء الاسم.
\n\nالفكرة الأساسية: التبعيات تتجه نحو الداخل. العالم الخارجي يعتمد على النواة، وليس العكس. هذا يسمح بتغيير قاعدة البيانات، أو بروتوكول الاتصال، أو واجهة المستخدم دون تغيير منطق الأعمال. يبدو وكأنه حلم؟ دعنا نرى كيف ننفذ ذلك في C++.
\n\nالمشكلة الكلاسيكية: الاقتران الوثيق
\n\nهذا ما يبدو عليه كود C++ النموذجي غالبًا:
\n\nclass OrderService {\npublic:\n void createOrder(const OrderData& data) {\n // منطق الأعمال\n if (data.items.empty()) throw std::runtime_error("الطلب فارغ");\n\n // وهنا المشكلة - اعتماد مباشر على قاعدة البيانات\n Database db("localhost", 5432);\n db.connect();\n db.saveOrder(data);\n db.disconnect();\n \n // وأيضًا على خدمة البريد الإلكتروني\n EmailSender sender("smtp.example.com");\n sender.sendInvoice(data);\n }\n};\n\nما الخطأ هنا؟ كل شيء. لا يمكنك اختبار createOrder بدون قاعدة بيانات حقيقية وخادم بريد إلكتروني. إذا قررت غدًا الانتقال من PostgreSQL إلى MongoDB - سيتعين عليك إعادة كتابة OrderService. وإذا أصبحت خدمة البريد الإلكتروني غير متزامنة عبر قائمة انتظار - مرة أخرى تعديلات في منطق الأعمال.
حل المشكلة باستخدام الهندسة السداسية
\n\nالخطوة 1: تحديد المنافذ (الواجهات)
\n\nالمنفذ هو عقد يحدد ما تحتاجه النواة من العالم الخارجي، ولكن ليس كيف يتم تنفيذه.
\n\n// port/OrderRepository.h\nclass OrderRepository {\npublic:\n virtual ~OrderRepository() = default;\n virtual void save(const Order& order) = 0;\n virtual std::optional<Order> findById(const std::string& id) = 0;\n};\n\n// port/NotificationService.h\nclass NotificationService {\npublic:\n virtual ~NotificationService() = default;\n virtual void sendInvoice(const Order& order) = 0;\n};\n\nلاحظ: هذه فئات مجردة خالصة. لا تبعيات لأطر العمل أو المكتبات أو الخدمات الخارجية. فقط كيانات الأعمال.
\n\nالخطوة 2: كتابة النواة (منطق الأعمال)
\n\nالآن خدمتنا تعتمد فقط على الواجهات، وليس على التطبيقات الملموسة:
\n\n// core/OrderService.h\nclass OrderService {\npublic:\n OrderService(OrderRepository& repo, NotificationService& notifier)\n : repository_(repo), notifier_(notifier) {}\n\n void createOrder(const OrderData& data) {\n // التحقق - منطق أعمال خالص\n if (data.items.empty()) {\n throw std::invalid_argument("لا يمكن أن يكون الطلب فارغًا");\n }\n\n Order order = Order::create(data);\n repository_.save(order);\n notifier_.sendInvoice(order);\n }\n\nprivate:\n OrderRepository& repository_;\n NotificationServ