مقدمة في Dependency Injection في C++
عندما تبدأ في تعلم C++، تكتب برامج بسيطة حيث تقوم الفئات بإنشاء تبعياتها مباشرة. على سبيل المثال، الفئة Car تنشئ بنفسها كائن Engine. هذا يعمل مع المشاريع الصغيرة، ولكن عندما يكبر الكود، يؤدي هذا النهج إلى مشاكل خطيرة: يصبح الكود مقترنًا بشدة، ويصعب اختباره وتعديله.
Dependency Injection (DI) هو نمط تصميم يحصل فيه الكائن على تبعياته من الخارج، بدلاً من إنشائها بنفسه. هذا أحد المبادئ الأساسية لـ SOLID، وتحديدًا مبدأ عكس التبعيات (Dependency Inversion Principle).
في هذه المقالة، سنشرح ما هو Dependency Injection في C++، ولماذا هو مطلوب، وكيفية تطبيقه عمليًا. سترى كيف يجعل هذا النمط الكود مرنًا وقابلًا للاختبار والصيانة.
لماذا بدون DI يكون الأمر سيئًا: مشكلة الاقتران الشديد
تخيل أنك تكتب نظامًا لإدارة المنزل الذكي. هناك فئة SmartLight تتحكم في المصباح. في التنفيذ البسيط، تقوم بإنشاء كائن Bulb بنفسها:
#include <iostream>#include <string>
class Bulb {public: void turnOn() { std::cout << "المصباح قيد التشغيل"; } void turnOff() { std::cout << "المصباح متوقف"; }};
class SmartLight {private: Bulb bulb; // تبعية صلبةpublic: void activate() { bulb.turnOn(); } void deactivate() { bulb.turnOff(); }};
int main() { SmartLight light; light.activate(); return 0;}ما هي المشاكل هنا؟
- الاقتران الشديد:
SmartLightمرتبط بشكل لا ينفصم بـBulb. إذا أردت استخدام مصباح آخر (مثل LED ذكي أو Phillips Hue)، سيتعين عليك تغيير كود الفئة. - صعوبة الاختبار: لا يمكنك استبدال المصباح الحقيقي بـ "وهمي" (mock) في اختبارات الوحدة.
- انتهاك SRP: فئة
SmartLightتقوم بأمرين: التحكم في المصباح وإنشائه.
هذا مثال كلاسيكي على الاقتران الشديد (tight coupling) الذي يهدف Dependency Injection إلى إزالته.
ما هو Dependency Injection في C++: المفاهيم الأساسية
Dependency Injection (DI) هو أسلوب يحصل فيه الكائن على تبعياته (عادةً عبر المُنشئ، أو setter، أو واجهة) من مصدر خارجي، بدلاً من إنشائها بنفسه. الفكرة الأساسية: "لا تسأل عما تحتاجه - سيتم إعطاؤك إياه".
في C++، يتم تنفيذ DI من خلال:
- الواجهات (الفئات المجردة): يتم تعريف التبعيات من خلال أنواع مجردة، وليس فئات محددة.
- المُنشئ مع المعاملات: يتم تمرير التبعيات عبر المُنشئ (الطريقة الأكثر شيوعًا).
- Setters: يتم تعيين التبعيات بعد إنشاء الكائن.
- حاويات التبعيات: مكتبات خاصة تقوم تلقائيًا بإنشاء وحقن التبعيات (مثل
boost.di).
دعنا نعيد كتابة مثال الضوء الذكي باستخدام DI:
#include <iostream>#include <memory>
// واجهة مجردة للمصباحclass IBulb {public: virtual void turnOn() = 0; virtual void turnOff() = 0; virtual ~IBulb() = default;};
// تنفيذ محدد: مصباح عاديclass Bulb : public IBulb {public: void turnOn() override { std::cout << "المصباح قيد التشغيل"; } void turnOff() override { std::cout << "المصباح متوقف"; }};
// تنفيذ آخر: مصباح LEDclass LedBulb : public IBulb {public: void turnOn() override { std::cout << "مصباح LED قيد التشغيل"; } void turnOff() override {