مقدمة: لماذا يعتبر حقن التبعيات مهمًا في C++؟
\n\nفي تطوير C++ الحديث، يُعد دعم الاقتران الضعيف (loose coupling) بين الفئات مفتاحًا للمرونة وقابلية اختبار الكود. حقن التبعيات (DI) أو Dependency Injection هو نمط يتم فيه استلام الكائن لتبعياته من الخارج، بدلاً من إنشائها بنفسه. وهذا يُطبق مباشرة مبدأ عكس التبعيات (Dependency Inversion Principle) من SOLID.
\n\nبدون DI، غالبًا ما تكون الفئات مرتبطة بشدة بتطبيقات محددة:
\n\n// سيء: الفئة تنشئ التبعية بنفسها\nclass ReportGenerator {\n Database db_; // ارتباط شديد بقاعدة بيانات محددة\npublic:\n ReportGenerator() : db_("localhost") {}\n void generate() { db_.query("SELECT ..."); }\n};\n\nمع DI نحصل على المرونة، وإمكانية تبديل التطبيقات (مثلًا لاختبارات الوحدة)، وتبسيط صيانة الكود. في هذه المقالة، سنستعرض ثلاث طرق عملية لتنفيذ DI في C++: عبر المُنشئ، عبر الواجهات، وباستخدام حاوية بسيطة.
\n\n1. الحقن عبر المُنشئ (Constructor Injection)
\n\nالطريقة الأكثر شيوعًا والموصى بها. يتم تمرير التبعيات إلى مُنشئ الفئة كمعاملات. في C++، من المناسب استخدام المؤشرات الذكية (std::unique_ptr, std::shared_ptr) أو المراجع.
مثال: مسجل وخدمة
\n\n#include <iostream>\n#include <memory>\n#include <string>\n\n// 1. التجريد (الواجهة)\nclass ILogger {\npublic:\n virtual ~ILogger() = default;\n virtual void log(const std::string& msg) = 0;\n};\n\n// 2. تطبيق محدد\nclass ConsoleLogger : public ILogger {\npublic:\n void log(const std::string& msg) override {\n std::cout << "[LOG]: " << msg << std::endl;\n }\n};\n\n// 3. الفئة التي تعتمد على ILogger\nclass UserService {\n std::shared_ptr<ILogger> logger_; // التبعية\npublic:\n // الحقن عبر المُنشئ\n explicit UserService(std::shared_ptr<ILogger> logger)\n : logger_(std::move(logger)) {}\n\n void createUser(const std::string& name) {\n // منطق الأعمال...\n logger_->log("User created: " + name);\n }\n};\n\nint main() {\n auto logger = std::make_shared<ConsoleLogger>();\n UserService service(logger);\n service.createUser("Alice");\n return 0;\n}\n\nالمزايا:
\n- \n
- التبعية واضحة في توقيع المُنشئ. \n
- الكائن دائمًا في حالة صالحة (بعد الإنشاء). \n
- سهولة تبديل التطبيق للاختبارات (مثلًا
MockLogger). \n
العيب: مع وجود عدد كبير من التبعيات، قد يصبح المُنشئ ضخمًا. في هذه الحالات، تساعد الحاويات (انظر القسم 3).
\n\n2. الحقن عبر الواجهات وتعدد الأشكال
\n\nهذا تطوير للنهج السابق. نحدد واجهات افتراضية بحتة لجميع التبعيات. يعمل كود العميل فقط مع التجريدات، بينما يتم إدخال التطبيقات المحددة في مرحلة تجميع الكائن (composition root).
\n\nمثال: مستودع ومنطق الأعمال
\n\n#include <vector>\n#include <memory>\n\n// واجهة المستودع\nclass IUserRepository {\npublic:\n virtual ~IUserRepository() = default;\n virtual void add(const std::string& user) = 0;\n virtual std::vector<std::string> getAll() const = 0;\n};\n\n// تطبيق لقاعدة البيانات\nclass DatabaseUserRepo : public IUserRepository {\npublic:\n void add(const std::string& user) override {\n // استعلام SQL\n }\n std::vector<std::string> getAll() const override {\n return {"Alice", "Bob"};\n }\n};\n\n// منطق الأعمال، لا يعرف شيئًا عن قاعدة البيانات\nclass UserManager {\n std::uniq