تعلم كيف يجعل حقن التبعيات الكود أسهل للاختبار، لإعادة الهيكلة، وللتوسع. استكشف أنماط عملية، أمثلة، والمزالق الشائعة لتتجنبها.

حقن التبعيات (Dependency Injection أو DI) فكرة بسيطة: بدلاً من أن يقوم جزء من الكود بإنشاء الأشياء التي يحتاجها، أنت تزوده بتلك الأشياء من الخارج.
تلك “الأشياء التي يحتاجها” هي التبعيات—على سبيل المثال، اتصال بقاعدة بيانات، خدمة دفع، ساعة، مسجل سجلات، أو مرسل بريد إلكتروني. إذا كان كودك يتولى بناء هذه التبعيات بنفسه، فهو يُغلق بهدوء على كيفية عمل هذه التبعيات.
فكر في آلة قهوة بالمكتب. تعتمد على الماء، حبوب القهوة، والكهرباء.
DI هو ذلك النهج الثاني: “آلة القهوة” (كلاسك/دالتك) تركز على صنع القهوة (وظيفتها)، بينما “المستلزمات” (التبعيات) يوفّرها من يقوم بإعدادها.
DI ليس مطلبًا لاستخدام إطار عمل محدد، وليس نفس شيء حاوية DI. يمكنك فعل DI يدويًا بتمرير التبعيات كمعاملات (أو عبر المُنشئات) وتنتهي.
DI أيضًا ليس "المحاكاة" (mocking). المحاكاة هي وسيلة محتملة لاستخدام DI في الاختبارات، لكن DI بحد ذاته مجرد خيار تصميم حول مكان إنشاء التبعيات.
عندما تُزوّد التبعيات من الخارج، يصبح كودك أسهل للتشغيل في سياقات مختلفة: الإنتاج، اختبارات الوحدة، العروض التوضيحية، والميزات المستقبلية.
تلك المرونة نفسها تجعل الوحدات أنظف: يمكن استبدال الأجزاء دون إعادة توصيل النظام بأكمله. نتيجة لذلك، تختصر الاختبارات وقت التشغيل وتصبح أوضح (لأنك تستطيع استبدالها بنماذج بسيطة)، ويصبح كود المشروع أسهل للتغيير (لأن الأجزاء أقل تشابكًا).
يحدث الاقتران الوثيق عندما يقرر جزء من الكود مباشرة ما الذي يجب أن يستخدمه من أجزاء أخرى. الشكل الأكثر شيوعًا بسيط: استدعاء new داخل منطق الأعمال.
تخيل دالة checkout التي تقوم داخليًا بـ new StripeClient() و new SmtpEmailSender(). في البداية يبدو ذلك مريحًا—كل ما تحتاجه موجود هناك. لكنه أيضًا يقفل تدفق الدفع على تلك التنفيذات بالذات، تفاصيل التكوين، وحتى قواعد الإنشاء (مفاتيح API، المهلات، سلوك الشبكة).
هذا الاقتران "مخفي" لأنّه ليس واضحًا من توقيع الدالة. تبدو الدالة وكأنها فقط تعالج طلبًا، لكنها تعتمد سرًا على بوابات دفع، مزودي بريد إلكتروني، وربما اتصال قاعدة بيانات أيضًا.
عندما تُرمّز التبعيات بشكل ثابت، حتى التغييرات الصغيرة تحدث تموجات:
التبعيات المُرمّزة تجعل اختبارات الوحدة تُشغّل عملًا حقيقيًا: استدعاءات شبكية، I/O للملفات، ساعات، معرفات عشوائية، أو موارد مشتركة. تصبح الاختبارات بطيئة لأنها غير معزولة، ومتقلبة لأن النتائج تعتمد على التوقيت، خدمات خارجية، أو ترتيب التنفيذ.
إذا رأيت هذه الأنماط، فربما يكون الاقتران الوثيق يكلفك بالفعل وقتًا:
new مبعثرة "في كل مكان" في منطق أساسييحل حقن التبعيات هذا بجعل التبعيات صريحة وقابلة للاستبدال—دون إعادة كتابة قواعد الأعمال في كل مرة يتغير فيها العالم.
Inversion of Control (IoC) هو تحول بسيط في المسؤولية: يجب أن يركز الكلاس على ما يحتاج أن يفعله، وليس كيفية الحصول على الأشياء التي يحتاجها.
عندما ينشئ الكلاس تبعياته بنفسه (مثال: new EmailService() أو فتح اتصال بقاعدة بيانات مباشرة)، فهو يتولى بهدوء وظيفتين: منطق الأعمال والإعداد. هذا يجعل الكلاس أصعب في التغيير، أصعب في إعادة الاستخدام، وأصعب في الاختبار.
مع IoC، يعتمد كودك على تجريدات—مثل الواجهات أو أنواع "العقد الصغيرة"—بدلاً من تنفيذات محددة.
مثال: CheckoutService لا يحتاج أن يعرف ما إذا كانت المدفوعات تتم عبر Stripe، PayPal، أو معالج اختبار زائف. يحتاج فقط إلى "شيء يمكنه خصم بطاقة". إذا قبل CheckoutService شيئًا من نوع IPaymentProcessor، فيمكنه العمل مع أي تنفيذ يتبع ذلك العقد.
هذا يُبقي منطقك الأساسي ثابتًا حتى عندما تتغير الأدوات الأساسية.
الجزء العملي من IoC هو نقل إنشاء التبعيات خارج الكلاس وتمريرها إليه (غالبًا عبر المُنشئ). هنا يأتي دور DI: DI وسيلة شائعة لتحقيق IoC.
بدلًا من:
تحصل على:
النتيجة هي المرونة: يصبح تبديل السلوك قرار تكوين، لا إعادة كتابة.
إذا لم تعد الكلاسات تنشئ تبعياتها، فشيء آخر يجب أن يفعل ذلك. ذلك الشيء هو الجذر التكويني: المكان الذي تُركَّب فيه تطبيقك—عادةً كود بدء التشغيل.
الجذر التكويني هو المكان الذي تقرر فيه، "في الإنتاج استخدم RealPaymentProcessor; في الاختبارات استخدم FakePaymentProcessor." إبقاء هذا الربط في مكان واحد يقلل المفاجآت ويحافظ على تركيز بقية الكود.
IoC يجعل اختبارات الوحدة أبسط لأنك تستطيع توفير بدائل اختبارية صغيرة وسريعة بدل استدعاء الشبكات أو قواعد البيانات الحقيقية.
كما يجعل إعادة الهيكلة أكثر أمانًا: عندما تفصل المسؤوليات، نادرًا ما يجبرك تغيير تنفيذ على تعديل الكلاسات التي تستخدمه—طالما ظل التجريد (الواجهة) نفسه.
DI ليست تقنية واحدة—إنها مجموعة صغيرة من الطرق لـ "تغذية" كلاس بالأشياء التي يعتمد عليها (مثل مسجل، عميل قاعدة بيانات، أو بوابة دفع). الأسلوب الذي تختاره يؤثر على الوضوح، القابلية للاختبار، ومدى سهولة سوء الاستخدام.
مع حقن المُنشئ، التبعيات مطلوبة لبناء الكائن. هذه ميزة كبيرة: لا يمكنك نسيانها بالخطأ.
هي الأنسب عندما تكون التبعية:
حقن المُنشئ ينتج عادةً كودًا أوضح وأسهل للاختبار، لأن اختبارك يمكنه تمرير وهمي أو محاكٍ عند الإنشاء.
أحيانًا تكون التبعية ضرورية لعملية واحدة فقط—مثل مُنسق مؤقت، استراتيجية خاصة، أو قيمة مرتبطة بطلب.
في هذه الحالات، مرّرها كمعامل دالة. هذا يبقي الكائن أصغر ويتجنب جعل حاجة لمرة واحدة حقلًا دائمًا.
حقن المُعّين يمكن أن يكون مريحًا عندما لا يمكنك توفير التبعية وقت الإنشاء (بعض الأطر أو مسارات كود قديمة). المقابل هو أنه قد يخفي المتطلبات: يبدو الكلاس قابلاً للاستخدام حتى لو لم يتم تكوينه بالكامل.
غالبًا يؤدي ذلك لمفاجآت وقت التشغيل ("لماذا هذا غير معرف؟") ويجعل الاختبارات أكثر هشاشة لأن الإعداد قد يُنسى بسهولة.
اختبارات الوحدة تكون أقوى عندما تكون سريعة، قابلة للتكرار، ومُركّزة على سلوك واحد. اللحظة التي تعتمد فيها اختبار وحدة على قاعدة بيانات حقيقية، استدعاء شبكة، نظام ملفات، أو وقت حقيقي، تميل إلى أن تصبح أبطأ ومتقلبة.
DI يصلح هذا بالسماح لكودك بقبول الأشياء التي يعتمد عليها (الوصول لقاعدة البيانات، عملاء HTTP، مزوّد الوقت) من الخارج. في الاختبارات تستطيع استبدال تلك التبعيات ببدائل خفيفة.
قاعدة بيانات حقيقية أو استدعاء API يضيفان زمن إعداد ومهلة. مع DI، يمكنك حقن مستودع داخل الذاكرة أو عميل زائف يعيد استجابات مُحضّرة فورًا. هذا يعني:
بدون DI، غالبًا ما يضطر الكود إلى "new()" لتبعياته، مما يجبر الاختبارات على تشغيل الستاك كله. مع DI، يمكنك حقن:
لا حيل، لا مفاتيح بيئة اختبارية عالمية—فقط مرّر تنفيذًا مختلفًا.
DI يجعل الإعداد صريحًا. بدل التنقيب في التكوين، سلاسل الاتصال، أو متغيرات بيئة اختبارية، تستطيع قراءة الاختبار ورؤية ما هو حقيقي وما هو مُستبدَل فورًا.
اختبار نموذجي صديق لـ DI يقرأ كالتالي:
Arrange: أنشئ الخدمة مع مستودع وهمي وساعة مزوّفة
Act: استدعِ الطريقة
Assert: تحقق من القيمة المرجعة و/أو تحقّق من تفاعلات الـ mock
تلك المباشرة تقلل الضوضاء وتجعل الأعطال أسهل للتشخيص—بالضبط ما تريده من اختبارات الوحدة.
فجوة الاختبار (test seam) هي "فتحة" مقصودة في كودك حيث يمكنك تبديل سلوك بآخر. في الإنتاج توصل الواقع، وفي الاختبارات توصل بديلاً أسرع وأكثر أمانًا. DI واحدة من أبسط الطرق لإنشاء هذه الفجوات دون حيل.
الفجوات مفيدة حول أجزاء من النظام يصعب التحكم بها في الاختبار:
إذا كان منطق الأعمال ينادي هذه الأشياء مباشرة، تصبح الاختبارات هشة: تفشل لأسباب غير مرتبطة بالمنطق (تقطّعات الشبكة، اختلاف المناطق الزمنية، ملفات مفقودة)، وتكون أصعب للتشغيل بسرعة.
غالبًا ما تأخذ الفجوة شكل واجهة—أو في لغات ديناميكية عقد بسيط مثل "هذا الكائن يجب أن يملك طريقة now()." الفكرة الأساسية هي الاعتماد على ما تحتاجه، لا من أين يأتي.
مثال: بدل استدعاء ساعة النظام مباشرة داخل خدمة الطلب، يمكنك الاعتماد على Clock:
SystemClock.now()FakeClock.now() يعيد وقتًا ثابتًاينطبق نفس النمط على قراءة الملفات (FileStore)، إرسال البريد (Mailer)، أو خصم البطاقات (PaymentGateway). يبقى منطقك الأساسي نفسه؛ يتغير فقط التنفيذ الموصل.
عندما يمكنك تبديل السلوك عن قصد:
الفجوات المتموضعة جيدًا تقلل الحاجة إلى المحاكاة المكثفة في كل مكان. بدلاً من ذلك، تحصل على نقاط استبدال نظيفة تحافظ على اختبارات الوحدة سريعة ومركزة ومتوقعة.
المعيارية فكرة أن برنامجك مبني من أجزاء مستقلة (وحدات) ذات حدود واضحة: كل وحدة تتحمل مسؤولية مركزة وطريقة تفاعل محددة مع بقية النظام.
DI يدعم ذلك بجعل تلك الحدود صريحة. بدل أن يستدعي الموديول نفسه إنشاء أو إيجاد كل ما يحتاجه، يستلم تبعيّاته من الخارج. هذا التحوّل الصغير يقلل مما يعرفه موديول عن الآخر.
عندما يبني الكود تبعياته داخليًا (مثلاً إنشاء عميل قاعدة بيانات داخل خدمة)، يصبح المستدعي والتبعية مرتبطين ارتباطًا وثيقًا. DI يشجعك على الاعتماد على واجهة (أو عقد) بدلاً من تنفيذ محدد.
هذا يعني أن الموديول عادة ما يحتاج إلى معرفة:
PaymentGateway.charge())نتيجةً لذلك، تتغير الوحدات معًا أقل كثيرًا، لأن التفاصيل الداخلية لا تتسرّب عبر الحدود.
من المفترض أن يسمح كود معياري باستبدال مكوّن دون إعادة كتابة كل من يستخدمه. DI يجعل هذا عمليًا:
في كل حالة، يستمر المستدعون في استخدام نفس العقد. يتغير "الربط" في مكان واحد (الجذر التكويني)، بدلًا من تعديلات مبعثرة في جميع أنحاء الكود.
حدود الاعتماد الواضحة تجعل العمل المتوازي أسهل. يمكن لفريق بناء تنفيذ جديد خلف واجهة متفق عليها بينما يستمر فريق آخر في تطوير ميزات تعتمد على تلك الواجهة.
DI يدعم أيضًا إعادة الهيكلة التدريجية: يمكنك استخراج موديول، حقنه، واستبداله تدريجيًا—بدون حاجة لكتابة شاملة.
ملاحظة: رؤية DI في الكود تجعلها مفهومة أسرع من أي تعريف. هنا مثال صغير لميزة إشعار.
عندما ينادي الكلاس new داخليًا، يقرر أي تنفيذ سيستخدم وكيف يبنيه.
class EmailService {
send(to, message) {
// talks to real SMTP provider
}
}
class WelcomeNotifier {
notify(user) {
const email = new EmailService();
email.send(user.email, "Welcome!");
}
}
مشكلة الاختبار: اختبار الوحدة قد يُشغّل سلوك البريد الحقيقي (أو يحتاج لتلقّي تغييرات عالمية محرجة).
test("sends welcome email", () => {
const notifier = new WelcomeNotifier();
notifier.notify({ email: "[email protected]" });
// Hard to assert without patching EmailService globally
});
الآن WelcomeNotifier يقبل أي كائن يطابق السلوك المطلوب.
class WelcomeNotifier {
constructor(emailService) {
this.emailService = emailService;
}
notify(user) {
this.emailService.send(user.email, "Welcome!");
}
}
يصبح الاختبار صغيرًا، سريعًا وصريحًا.
test("sends welcome email", () => {
const fakeEmail = { send: vi.fn() };
const notifier = new WelcomeNotifier(fakeEmail);
notifier.notify({ email: "[email protected]" });
expect(fakeEmail.send).toHaveBeenCalledWith("[email protected]", "Welcome!");
});
أرادَت دعم SMS لاحقًا؟ لا تمس WelcomeNotifier. فقط مرّر تنفيذًا مختلفًا:
const smsService = { send: (to, msg) => {/* SMS provider */} };
const notifier = new WelcomeNotifier(smsService);
هذا هو العائد العملي: الاختبارات تتوقف عن محاربة تفاصيل الإنشاء، وتُضاف سلوكيات جديدة عن طريق تبديل التبعيات بدل إعادة كتابة الكود القائم.
DI يمكن أن يكون بسيطًا مثل "مرّر الشيء الذي تحتاجه إلى الشيء الذي يستخدمه." هذا DI يدوي. حاوية DI أداة تقوم بأتمتة الربط. كلاهما قد يكون اختيارًا صحيحًا—المفتاح هو اختيار مستوى الأتمتة الذي يناسب تطبيقك.
مع DI اليدوي، تنشئ الكائنات بنفسك وتمرّر التبعيات عبر المُنشئات (أو المعاملات). الأمر واضح:
الربط اليدوي يجبر أيضًا على عادات تصميم جيدة. إذا احتاج كائن إلى سبع تبعيات، ستشعر بالألم فورًا—غالبًا إشارة لتقسيم المسؤوليات.
مع تزايد عدد المكونات، قد يصبح الربط اليدوي تكراريًا. حاوية DI يمكن أن تساعد عن طريق:
تتألق الحاويات في التطبيقات ذات الحدود والدورات الواضحة—تطبيقات الويب، خدمات طويلة التشغيل، أو أنظمة تعتمد ميزات مشتركة.
حاوية قد تجعل تصميمًا عالي الاقتران "يبدو" مرتبًا لأن الربط يختفي. لكن المشكلات الأساسية تبقى:
إذا جعلت إضافة الحاوية الكود أقل قابلية للقراءة، أو توقف الفريق عن معرفة من يعتمد على ماذا، فربما ذهبت بعيدًا.
ابدأ بـ DI اليدوي للحفاظ على الوضوح أثناء تشكيل الموديولات. أضف حاوية عندما يصبح الربط مملًا أو عندما تحتاج إدارة دورة حياة.
قاعدة عملية: استخدم DI اليدوي داخل الكود الأساسي/منطق الأعمال، و(اختياريًا) حاوية عند حد التطبيق (الجذر التكويني) لتجميع كل شيء. هذا يحافظ على تصميم واضح مع تقليل الأعمال المتكررة عندما يكبر المشروع.
DI يمكن أن يجعل الكود أسهل للاختبار والتغيير—لكن فقط إذا استُخدمت بانضباط. فيما يلي أكثر الطرق الشائعة لفشل DI، وعادات تُبقيه مفيدًا.
إذا احتاج كلاس لقائمة طويلة من التبعيات، غالبًا ما يعني أنه يفعل الكثير. هذه ليست فشلًا في DI—إنها DI تكشف رائحة تصميم.
قاعدة عملية: إذا لم تستطع وصف وظيفة الكلاس في جملة واحدة، أو إن المُنشئ يواصل النمو، فكّر في تقسيم الكلاس، استخراج متعاون أصغر، أو تجميع عمليات متقاربة خلف واجهة واحدة (بحذر—لا تخلق خدمات إلهية).
نمط Service Locator يبدو عادةً كاستدعاء container.get(Foo) داخل منطق الأعمال. يبدو مريحًا، لكنه يجعل التبعيات غير مرئية: لا يمكنك معرفة ما يحتاجه الكلاس بقراءة المُنشئ.
يصبح الاختبار أصعب لأنك تحتاج لإعداد حالة عالمية (المحدّد) بدل تزويد مجموعة واضحة ومحلية من البدائل. فضّل تمرير التبعيات صراحة (حقن المُنشئ أبسط الطرق).
حاويات DI قد تفشل وقت التشغيل عندما:
تلك المشاكل محبطة لأنها تظهر فقط عندما ينفّذ الربط.
حافظ على المُنشئات صغيرة ومركزة. إذا نمت قائمة التبعيات، اعتبرها حافزًا لإعادة التصميم.
أضف اختبارات تكاملية للربط. حتى اختبار بسيط "يبني الحاوية" أو يشيّد الربط اليدوي يمكن أن يكتشف التسجيلات المفقودة والدورات مبكرًا—قبل الإنتاج.
وأخيرًا، احتفظ بإنشاء الكائنات في مكان واحد (غالبًا بدء التطبيق/الجذر التكويني) وابتعد عن استدعاءات الحاوية داخل منطق الأعمال. هذا يحافظ على الفائدة الأساسية لـ DI: وضوح من يعتمد على ماذا.
DI أسهل عند اعتبارها سلسلة من إعادة الهيكلة الصغيرة والمنخفضة المخاطر. ابدأ حيث الاختبارات بطيئة أو متقلبة، وحيث التغييرات تنتشر عبر كود غير ذي صلة.
ابحث عن تبعيات تجعل الكود صعب الاختبار أو الفهم:
إذا كانت دالة لا تعمل بدون الوصول خارج العملية، فعادةً ما تكون مرشحًا جيدًا.
new.تجعل هذه المقاربة كل تغيير قابلًا للمراجعة وتتيح التوقف بعد أي خطوة دون تعطيل النظام.
قد يحوّل DI الكود بطريق الخطأ إلى "الكل يعتمد على الكل" إذا حقنت الكثير. قاعدة جيدة: حقن القدرات، لا التفاصيل. مثال: حقن Clock بدل "SystemTime + TimeZoneResolver + NtpClient".
إذا احتاج الكلاس لخمسة خدمات غير مرتبطة، فقد يقوم بالكثير—فكر في تقسيمه. أيضًا، تجنّب تمرير التبعيات عبر طبقات متعددة "احتمالًا"؛ حقن فقط حيث تُستخدم، ومركّز الربط في مكان واحد.
إذا كنت تستخدم مولد كود أو سير عمل سريع لتوليد ميزات، يصبح DI أكثر قيمة لأنه يحافظ على البنية أثناء نمو المشروع. على سبيل المثال، عندما تستخدم Koder.ai لإنشاء واجهات React، خدمات Go، وقواعد بيانات PostgreSQL من مواصفات مُدارة عبر الدردشة، يساعد وجود جذر تكويني واضح وواجهات صديقة للاختبار في الحفاظ على الكود المولد سهل الاختبار وإعادة الهيكلة واستبدال التكاملات (البريد، المدفوعات، التخزين) دون إعادة كتابة منطق الأعمال.
القاعدة تبقى نفسها: احتفظ بإنشاء الكائنات والربط الخاص بالبيئة عند الحدود، واجعل كود الأعمال مركزًا على السلوك.
يجب أن تتمكن من الإشارة إلى تحسينات ملموسة:
إن أردت خطوة تالية، وثّق "الجذر التكويني" واجعله ملفًا واحدًا بسيطًا للربط، بينما يبقى بقية الكود مركّزًا على السلوك.
Dependency Injection (DI) يعني أن الكود الخاص بك يتلقى الأشياء التي يحتاجها (قاعدة بيانات، مسجل سجلات، ساعة، عميل دفع) من الخارج بدلاً من إنشائها داخلياً.
عملياً، يظهر ذلك عادةً بتمرير التبعيات إلى مُنشئ (constructor) أو كمعامل دالة بحيث تكون صريحة ويمكن استبدالها.
Inversion of Control (IoC) هي الفكرة الأوسع: يجب أن يركز الكلاس على ما يفعله، لا على كيفية الحصول على متعاونيه.
DI هي تقنية شائعة لتحقيق IoC عن طريق نقل إنشاء التبعيات إلى الخارج وتمريرها في.
إذا تم إنشاء تبعية باستخدام new داخل منطق الأعمال، يصبح من الصعب استبدالها.
ذلك يؤدي إلى:
DI تساعد الاختبارات على البقاء سريعة وحتمية لأنك تستطيع حقن بدائل اختبارية بدلاً من الاعتماد على أنظمة خارجية حقيقية.
استبدالات شائعة:
حاوية DI اختيارية. ابدأ بـ DI اليدوي (مرّر التبعيات صراحة) عندما:
فكّر في استخدام حاوية عندما يصبح الربط متكررًا أو تحتاج إدارة دورة حياة (singleton/طلب/مؤقت).
استخدم حقن المُنشئ (constructor) عندما تكون التبعية مطلوبة لعمل الكائن وتُستخدم عبر طرق متعددة.
استخدم حقن المعامل/الطريقة (method/parameter) عندما تكون الحاجة لنداء واحد فقط (مثال: قيمة مرتبطة بالطلب).
تجنّب حقن المُعّين/الخاصية (setter/property) إلا إن كنت بحاجة فعلًا إلى الربط المتأخر؛ أضف تحققًا للفشل السريع إن اعتمدت عليه.
الجذر التكويني (composition root) هو المكان الذي تجمع فيه التطبيق: تنشئ التنفيذات وتمرّرها إلى الخدمات التي تحتاجها.
احتفظ به بالقرب من نقطة بدء التطبيق (entry point) حتى يبقى بقية الكود مركزًا على السلوك لا على الربط.
فجوة الاختبار (test seam) هي نقطة مقصودة حيث يمكن تبديل السلوك.
أماكن جيدة للفجوات فهي الاهتمامات الصعبة للاختبار:
Clock.now())DI يخلق فجوات عبر السماح لك بحقن تنفيذ بديل في الاختبارات.
الأخطاء الشائعة تشمل:
container.get() داخل منطق الأعمال يخفي التبعيات الحقيقية؛ فضّل المعاملات الصريحة.اتبع إعادة هيكلة صغيرة وقابلة للتكرار:
كرر للمناطق التالية؛ يمكنك التوقف في أي وقت دون إعادة كتابة شاملة.