Dependency Injection هي تقنية شائعة الاستخدام في البرمجة الموجهة للكائنات المصممة لتقليل اتصال المكونات. عند استخدامها بشكل صحيح ، بالإضافة إلى تحقيق هذا الهدف ، فإنه يمكن أن يحقق صفات سحرية حقًا في تطبيقاتك. مثل أي سحر ، يُنظر إلى هذه التقنية على أنها مجموعة من التعاويذ ، وليست أطروحة علمية صارمة. هذا يؤدي إلى سوء تفسير للظواهر ، ونتيجة لذلك ، إلى إساءة استخدام القطع الأثرية. في مقالتي الرسمية ، أقترح أن يخطو القارئ خطوة بخطوة ، باختصار وبشكل جوهري ، المسار المنطقي من الأسس المناسبة للتصميم الموجه إلى الكائن إلى سحر الحقن التلقائي التبعية.
تعتمد المادة على تطوير
حاوية Hypo IoC ، والتي ذكرتها في
مقال سابق . في أمثلة الكود المصغر ، سأستخدم روبي كواحدة من أكثر اللغات موجزة وجوه المنحى لكتابة أمثلة قصيرة. هذا لا ينبغي أن يسبب مشاكل للمطورين في لغات أخرى لفهم.
المستوى 1: مبدأ انعكاس التبعية
يواجه المطورون في النموذج الموجه للكائنات إنشاء كائنات كل يوم ، والتي بدورها قد تعتمد على كائنات أخرى. هذا يؤدي إلى الرسم البياني التبعية. لنفترض أننا نتعامل مع نموذج كائن من النموذج:
- بعض خدمات الفوترة (InvoiceProcessor) وخدمة الإعلام (NotificationService). ترسل خدمة معالجة الفاتورة إعلامات عند استيفاء شروط معينة ، وسنتخذ هذا المنطق خارج النطاق. من حيث المبدأ ، هذا النموذج جيد بالفعل في أن المكونات الفردية مسؤولة عن المسؤوليات المختلفة. تكمن المشكلة في كيفية تطبيق هذه التبعيات. الخطأ الشائع هو تهيئة التبعية حيث يتم استخدام هذه التبعية:
class InvoiceProcessor def process(invoice)
هذا خطأ نظرًا لحقيقة أننا نحصل على اتصال عالي بالكائنات المستقلة منطقياً (High Coupling). يؤدي هذا إلى انتهاك مبدأ المسؤولية الفردية - يجب أن يقوم الكائن التابع ، بالإضافة إلى مسؤولياته المباشرة ، بتهيئة التبعيات الخاصة به ؛ وأيضًا "تعرف" على واجهة مُنشئ التبعية ، والتي ستؤدي إلى سبب إضافي للتغيير (
"سبب التغيير" ، ر. مارتن ). من الأصح تمرير هذا النوع من التبعية ، التهيئة خارج الكائن التابع:
class InvoiceProcessor def initialize(notificationService) @notificationService = notificationService end def process(invoice) @notificationService.notify(invoice.owner) end end notificationService = NotificationService.new invoiceProcessor = InvoiceProcessor.new(notificationService)
يتوافق هذا النهج مع مبدأ انعكاس التبعية. نحن الآن ننقل كائنًا بواجهة إرسال الرسائل - لم تعد هناك حاجة لخدمة الفواتير "لمعرفة" كيفية إنشاء كائن خدمة الإعلام. عند كتابة اختبارات وحدة خدمة معالجة الفاتورة ، لا يتعين على المطور أن يتغلب على كيفية استبدال تطبيق واجهة خدمة الإشعارات بكعب. في اللغات ذات الكتابة الديناميكية ، مثل Ruby ، يمكنك استبدال أي كائن يلبي طريقة الإخطار ؛ من خلال الكتابة الثابتة ، مثل C # / Java ، يمكنك استخدام واجهة INotificationService ، والتي من السهل إنشاء Mock. تم الكشف عن قضية انعكاس التبعية بالتفصيل من قبل ألكساندر بينديو
في مقال احتفل مؤخرًا بمرور 10 أعوام
على تأسيسه!
المستوى 2: تسجيل الكائنات ذات الصلة
استخدام مبدأ انعكاس التبعية لا يبدو ممارسة معقدة. ولكن مع مرور الوقت ، وبسبب زيادة عدد الأشياء والعلاقات ، تظهر تحديات جديدة. يمكن استخدام خدمة الإعلام بواسطة خدمات أخرى غير InvoiceProcessor. بالإضافة إلى ذلك ، قد يعتمد هو نفسه على خدمات أخرى ، والتي بدورها تعتمد على خدمات ثالثة ، إلخ. أيضًا ، قد لا يتم استخدام بعض المكونات دائمًا في نسخة واحدة. المهمة الرئيسية هي العثور على إجابة على السؤال - "متى يتم إنشاء التبعيات؟".
لحل هذه المشكلة ، يمكنك محاولة إنشاء حل يستند إلى مجموعة اقتران التبعيات. قد تبدو واجهة مثال لعمله كما يلي:
registry.add(InvoiceProcessor) .depends_on(NotificationService) registry.add(NotificationService) .depends_on(ServiceX) invoiceProcessor = registry.resolve(InvoiceProcessor) invoiceProcessor.process(invoice)
ليس من الصعب تنفيذها في الممارسة العملية:
في كل مرة يتم استدعاء container.resolve () ، سننتقل إلى المصنع ، الذي سينشئ مثيلات التبعية ، متجاوزًا بشكل متكرر الرسم البياني للتبعية الموضح في السجل. في حالة `container.resolve (InvoiceProcessor)` ، سيتم تنفيذ ما يلي:
- factory.resolve (InvoiceProcessor) - يطلب المصنع التبعيات InvoiceProcessor في السجل ، يتلقى خدمة الإعلام ، والتي تحتاج أيضًا إلى تجميع.
- factory.resolve (NotificationService) - يطلب المصنع تبعيات NotificationService في السجل ، ويتلقى ServiceX ، الذي يحتاج أيضًا إلى التجميع.
- factory.resolve (ServiceX) - لا يوجد لديه تبعيات ، وإنشاء ، والعودة على طول مكدس الاستدعاء إلى الخطوة 1 ، الحصول على كائن مجمعة من نوع InvoiceProcessor.
قد يعتمد كل مكون على عدة مكونات أخرى ، لذا فإن السؤال الواضح هو "كيفية مطابقة معلمات المصمم بشكل صحيح مع مثيلات التبعية الناتجة؟". مثال:
class InvoiceProcessor def initialize(notificationService, paymentService)
في اللغات ذات الكتابة الثابتة ، يمكن أن يكون نوع المعلمة بمثابة محدد:
class InvoiceProcessor { constructor(notificationService: NotificationService, paymentService: PaymentService) {
ضمن Ruby ، يمكنك استخدام الاصطلاح - ما عليك سوى استخدام اسم الكتابة بتنسيق snake_case ، وسيكون هذا هو اسم المعلمة المتوقع.
المستوى 3: إدارة مدى الحياة التبعية
لدينا بالفعل حل جيد لإدارة التبعية. القيد الوحيد هو الحاجة إلى إنشاء مثيل جديد من التبعية مع كل مكالمة. ولكن ماذا لو لم نتمكن من إنشاء أكثر من مثيل واحد للمكون؟ على سبيل المثال ، مجموعة من الاتصالات بقاعدة البيانات. حفر أعمق ، وإذا كنا بحاجة إلى توفير حياة تسيطر عليها من التبعيات؟ على سبيل المثال ، أغلق الاتصال بقاعدة البيانات بعد إتمام طلب HTTP.
يصبح من الواضح أن المرشح للاستبدال في الحل الأصلي هو InstanceFactory. تحديث المخطط:
والحل المنطقي هو استخدام مجموعة من الاستراتيجيات (
الإستراتيجية ، GoF ) للحصول على مثيلات المكونات. الآن لا نقوم دائمًا بإنشاء مثيلات جديدة عند استدعاء Container :: solution ، لذلك من المناسب إعادة تسمية Factory إلى Resolver. يرجى ملاحظة أن أسلوب Container :: register يحتوي على معلمة جديدة - مدى الحياة (العمر). هذه المعلمة اختيارية - بشكل افتراضي ، تكون قيمتها "عابرة" (عابرة) ، والتي تتوافق مع السلوك المطبق مسبقًا. إستراتيجية المفردة واضحة أيضًا - باستخدامها يتم إنشاء مثيل واحد فقط للمكون ، والذي سيتم إرجاعه في كل مرة.
النطاق هو استراتيجية أكثر تعقيدًا قليلاً. بدلاً من "مسارات عابرة" و "وحيدون" ، غالبًا ما يكون مطلوبًا استخدام شيء ما - مكون موجود طوال عمر مكون آخر. مثال مماثل يمكن أن يكون كائن طلب تطبيق ويب ، وهو سياق وجود مثل هذه الأشياء ، على سبيل المثال ، معلمات HTTP ، اتصال قاعدة البيانات ، تجميعات النماذج. خلال عمر الطلب ، نقوم بجمع واستخدام هذه التبعيات ، وبعد تدميرها ، نتوقع أن يتم تدميرها جميعًا. لتنفيذ هذه الوظيفة ، سيكون من الضروري تطوير بنية كائن مغلقة إلى حد ما:
يُظهر الرسم التخطيطي جزءًا يعكس التغييرات في فئتي Component و LifetimeStrategy في سياق تنفيذ عمر النطاق. كانت النتيجة نوعا من "الجسر المزدوج" (على غرار قالب
الجسر ، GoF ). باستخدام تعقيدات تقنيات الميراث والتجميع ، يصبح المكون هو جوهر الحاوية. بالمناسبة ، يحتوي المخطط على ميراث متعددة. حيث تسمح لغة البرمجة والضمير بذلك ، يمكنك ترك الأمر بهذه الطريقة. في روبي الأول استخدم الشوائب ، في لغات أخرى يمكنك استبدال الميراث بجسر آخر:
يعرض الرسم التخطيطي للتسلسل دورة حياة مكون الجلسة ، والذي يرتبط بعمر مكون الطلب:

كما ترون من المخطط ، في وقت معين ، عندما يكمل مكون الطلب مهمته ، يتم استدعاء طريقة التحرير ، والتي تبدأ عملية تدمير النطاق.
المستوى 4: حقن التبعية
حتى الآن ، تحدثت عن كيفية تحديد سجل التبعيات ، ثم كيفية إنشاء وتدمير المكونات وفقًا للرسم البياني للعلاقات المشكلة. ولماذا؟ لنفترض أننا نستخدم هذا كجزء من روبي أون ريلز:
class InvoiceController < ApplicationController def pay(params) invoice_repository = registry.resolve(InvoiceRepository) invoice_processor = registry.resolve(InvoiceProcessor) invoice = invoice_repository.find(params[:id]) invoice_processor.pay(invoice) end end
الرمز الذي سيتم كتابته بهذه الطريقة لن يكون أكثر قابلية للقراءة أو قابلاً للاختبار أو المرونة. لا يمكننا "إجبار" القضبان على حقن تبعيات وحدة التحكم من خلال مُنشئها ؛ وهذا لا يوفره الإطار. ولكن ، على سبيل المثال ، في ASP.NET MVC يتم تطبيق ذلك على المستوى الأساسي. للاستفادة إلى أقصى حد من استخدام آلية دقة التبعية التلقائية ، تحتاج إلى تطبيق تقنية انعكاس التحكم (IoC ، انعكاس التحكم). هذا هو النهج الذي تتجاوز فيه مسؤولية حل التبعيات نطاق رمز التطبيق ويعتمد على الإطار. النظر في مثال.
تخيل أننا نقوم بتصميم شيء مثل Rails من الصفر. نطبق المخطط التالي:
يتلقى التطبيق الطلب ، ويستعيد جهاز التوجيه المعلمات ويوجه وحدة التحكم المناسبة لمعالجة هذا الطلب. يعمل هذا المخطط على نسخ سلوك إطار ويب نموذجي بشكل مشروط بفارق بسيط فقط - حاوية IoC متورطة في إنشاء التبعيات وتنفيذها. لكن السؤال الذي يطرح نفسه هنا ، أين يتم إنشاء الحاوية نفسها؟ من أجل تغطية أكبر عدد ممكن من كائنات التطبيق المستقبلي ، يجب أن ينشئ إطارنا حاوية في المرحلة المبكرة للغاية من تشغيله. من الواضح ، لا يوجد مكان أكثر ملاءمة من تطبيق باني التطبيق. إنه أيضًا المكان الأنسب لتكوين كل التبعيات:
class App
أي تطبيق لديه نقطة دخول ، على سبيل المثال ، الطريقة الرئيسية. في هذا المثال ، نقطة الدخول هي طريقة الاتصال. الهدف من هذه الطريقة هو استدعاء الموجه لمعالجة الطلبات الواردة. يجب أن تكون نقطة الدخول هي المكان الوحيد للاتصال بالحاوية مباشرة - منذ تلك اللحظة يجب أن تمر الحاوية على جانب الطريق ، يجب أن يحدث كل السحر اللاحق "تحت الغطاء". تنفيذ وحدة تحكم داخل مثل هذه الهندسة تبدو حقا غير عادية. على الرغم من حقيقة أننا لا نستنسخه بشكل صريح ، إلا أنه يحتوي على مُنشئ ذو معلمات:
class Controller
البيئة "يفهم" كيفية إنشاء مثيلات وحدة التحكم. هذا ممكن بفضل آلية حقن التبعية التي توفرها حاوية IoC المضمنة في قلب تطبيق الويب. في مُنشئ وحدة التحكم ، يمكنك الآن سرد كل ما هو مطلوب لتشغيله. الشيء الرئيسي هو أن المكونات المقابلة مسجلة في الحاوية. الآن دعنا ننتقل إلى تنفيذ جهاز التوجيه:
class Router
لاحظ أن جهاز التوجيه يعتمد على جهاز التحكم. إذا استدعينا إعدادات التبعية ، فستكون وحدة التحكم مكونًا قصير العمر ، ويكون جهاز التوجيه ثابتًا. كيف يمكن أن يكون هذا؟ الجواب هو أن المكونات ليست مثيلات للفئات المقابلة ، كما تبدو خارجيًا. في الواقع ، هذه هي كائنات الوكيل (
Proxy، GoF ) مع مثيل أسلوب المصنع (
أسلوب المصنع ، GoF ) ؛ يقومون بإرجاع مثيل المكون وفقًا للاستراتيجية المعينة. نظرًا لأن وحدة التحكم مسجلة على أنها "عابرة" ، فسيتعامل جهاز التوجيه دائمًا مع مثيله الجديد عند الوصول إليه. يُظهر مخطط التسلسل آلية تقريبية للعمل:
أي بالإضافة إلى إدارة التبعية ، يتحمل إطار عمل جيد قائم على حاوية IoC مسؤولية الإدارة الصحيحة لأعمار المكونات.
استنتاج
يمكن أن يكون لتقنية Dependency Injection تنفيذ داخلي متطور إلى حد ما. هذا هو ثمن نقل تعقيد تنفيذ التطبيقات المرنة إلى لب الإطار. لا يمكن لمستخدم مثل هذه الأطر القلق بشأن الجوانب التقنية البحتة ، ولكن يخصص مزيدًا من الوقت لتطوير مريح لمنطق الأعمال لبرامج التطبيق. باستخدام تطبيق DI عالي الجودة ، يكتب مبرمج التطبيق في البداية رمزًا قابلاً للاختبار ومدعوم جيدًا. من الأمثلة الجيدة على تطبيق Dependency Injection ، إطار
Dandy الموصوف في مقالتي السابقة
Orthodox Backend .