مقدمة إلى حقن التبعية في لغة C++
حقن التبعية (Dependency Injection - DI) هو أحد أنماط التصميم الأساسية التي تطبق مبدأ عكس التحكم (Inversion of Control - IoC). في سياق لغة C++، يسمح DI بإنشاء تطبيقات ضعيفة الارتباط وقابلة للاختبار وسهلة الصيانة. بدلاً من أن يقوم الفصل (class) بإنشاء تبعياته بنفسه (الكائنات التي يعتمد عليها)، يتم تمرير هذه التبعيات (حقنها) من الخارج.
تخيل مثالاً كلاسيكياً: الفصل Car يستخدم محركاً. بدون DI، يقوم Car بإنشاء مثيل محدد من Engine بنفسه. مع DI، يتلقى Car كائن Engine جاهزاً عبر المُنشئ (constructor). هذا يغير البنية بشكل جذري، مما يجعل الكود مرناً ومناسباً لاختبارات الوحدة (unit testing).
لماذا يعتبر حقن التبعية مهماً لمشاريع C++؟
تطبيق DI في لغة C++ يحل العديد من المشكلات الحرجة التي تظهر أثناء تطوير الأنظمة المعقدة.
مزايا استخدام DI
- الارتباط الضعيف (Loose Coupling): تعتمد الفصول على التجريدات (الواجهات) بدلاً من التطبيقات الملموسة. هذا يسمح باستبدال تطبيق بآخر بسهولة (مثل استبدال قاعدة بيانات حقيقية بكائن وهمي للاختبارات).
- تبسيط الاختبار: يمكنك استخدام mock-objects أو stub-objects لعزل الفصل الذي تختبره. هذا هو حجر الزاوية في اختبارات الوحدة.
- زيادة إعادة استخدام الكود: تصبح المكونات معيارية ويمكن إعادة استخدامها في سياقات مختلفة.
- دعم مبادئ SOLID: يدعم DI بشكل مباشر مبدأ عكس التبعية (Dependency Inversion Principle - DIP) - يجب ألا تعتمد الوحدات عالية المستوى على الوحدات منخفضة المستوى؛ بل يجب أن يعتمد كلاهما على التجريدات.
المشكلات التي يحلها DI
- الارتباط الصلب: بدون DI، يتطلب تغيير تطبيق التبعية (مثل تغيير نظام إدارة قواعد البيانات) تغيير الكود في جميع الفصول التي تستخدمه.
- صعوبة الاختبار: من المستحيل اختبار فصل يقوم داخلياً بإنشاء كائن يتعامل مع نظام الملفات أو الشبكة.
- صعوبات التهيئة: من الصعب إدارة دورة حياة الكائنات وتهيئتها في المشاريع الكبيرة.
طرق تطبيق حقن التبعية في لغة C++
توجد ثلاث طرق رئيسية لحقن التبعيات في لغة C++. دعنا نستعرض كل منها مع أمثلة على الكود.
1. الحقن عبر المُنشئ (Constructor Injection)
الطريقة الأكثر شيوعاً والمفضلة. يتم تمرير التبعيات إلى مُنشئ الفصل. هذا يضمن أن الكائن يكون دائماً في حالة صالحة.
#include <iostream>#include <memory>
// التجريد (الواجهة)class ILogger {public: virtual ~ILogger() = default; virtual void log(const std::string& message) = 0;};
// التطبيق الملموس 1class ConsoleLogger : public ILogger {public: void log(const std::string& message) override { std::cout << "Console: " << message << std::endl; }};
// التطبيق الملموس 2 (للاختبارات)class MockLogger : public ILogger {public: void log(const std::string& message) override { // لا نفعل شيئاً، مجرد دمية }};
// الفصل الذي يستخدم DIclass UserService {private: std::unique_ptr<ILogger> logger_; // الاعتماد على التجريدpublic: // الحقن عبر المُنشئ explicit UserService(std::unique_ptr<ILogger> logger) : logger_(std::move(logger)) {}
void createUser(const std::string& name) { // ... منطق الأعمال ... logger_->log("User created: " + name); }};
int main() { // حقن المسجل الحقيقي auto service = UserService(std::m