مدونة عمار الخوالده

حقن الاعتماديات - Dependency Injection


حقن الاعتماديات - Dependency Injection

مقدمة

أثناء البرمجة نحتاج أحيانا إلى عمل تعديلات على المشروع، هذه التعديلات قد تكون تغييرا لخدمة معينة، او لتقنية مستخدمة في المشروع، على سبيل المثال قد يكون مشروعي يستخدم Memcached لتخزين الـ Cache الخاص بالمشروع، ثم قد أقرر أن Redis أنسب لهذا للمشروع، أو ربما أقوم بتغيير الخدمة التي أتعامل معها في عمليات الدفع أو إرسال رسائل الـ SMS.

لنفترض أننا نتعامل مع Paypal كخدمة دفع في مشروعنا، ثم قررنا الانتقال إلى Stripe، سنضطر في هذه الحالة إلى تعديل أي مكان في الكود يتعامل مع Paypal واستدعاء الـ Functions الخاصة بـ Stripe كبديل.

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

طوال فترة حياة المشروع، ستكون هناك بالغالب الكثير من التغييرات الشبيهة بالمثال السابق، لذلك يجب علينا التعامل بشكل منظم مع عناصر المشروع قبل زيادة حجم المشكلة، بحيث يصبح تبديل أي عنصر من عناصر المشروع أسهل مستقبلا.

ملاحظة: تنظيم المشروع يعتمد على الكثير من من العوامل، هذا المقال يشرح فقط طريقة الاستفادة من الـ Dependincy Injection دون التطرق لباقي المواضيع.

Interfaces

لتخفيف التعقيد في الحالة السابقة يمكن استخدام الـ Interface لتنظيم الأمور أكثر، الـ Interface أداة تنظيمية تستخدم لإلزام المبرمجين الذين يستخدمون هذه الـ Interface على كتابة الـ Classes بطريقة موحدة.

فهي أشبه بعَقد مُلزم لجميع المبرمجين، يُلزمهم بوجود تعريف بطريقة محددة للـ Methods داخل الـ Class.

على سبيل المثال، يمكن تعريف Interface مخصصة لخدمات الدفع بهذا الشكل:

 1<?php
 2
 3use App\Models\Invoice;
 4
 5interface PaymentService
 6{
 7    public function payInvoice(Invoice $invoice);
 8
 9    public function refundInvoice(Invoice $invoice);
10}

عند التعامل مع أي خدمة دفع جديدة، سنقوم بإنشاء Class جديد يقوم بعمل implement للـ Interface الخاصة بخدمات الدفع، بهذه الطريقة سنضمن أن أي Class خاص بخدمة دفع سيكون عنده بالتأكيد نفس الـ Methods الأساسية التي نستخدمها في المشروع.

صورة توضيحية لكلاسات مختلفة تطبق interface خاصة بخدمات الدفع

لاحظ الآن أن StripeService و PaypalService كلاهما يقومان بعمل implement للـ interface، عند تعريف أي parameter داخل أي مشروع، لا يجب أن نستخدم StripeService أو PaypalService، بل نقوم باستخدام PaymentService، بهذه الطريقة يمكننا تمرير PaypalService أو StripeService أو أي Class سنقوم بإنشائه مستقبلا.

مثال:

1public function createInvoice(Request $request, PaymentService $paymentService)
2{
3    $invoice = new Invoice();
4    // ...
5
6    $paymentService->pay($invoice);
7}

لاحظ أننا في المثال السابق قمنا بتعريف الـ Parameter على أنه من نوع PaymentService، مما يمكننا بسهولة من تمرير أي كلاس يقوم بعمل implement لهذا الـ interface*، بهذه الطريقة في حال احتجنا لإضافة أي خدمة جديدة، بدلا من تعديل الكود في جميع ملفات المشروع، نقوم فقط بإنشاء Class جديد ينفذ العمليات الرئيسية المُعرفة داخل الـ interface، ثم نقوم بتمريره بدلا من الـ Class السابق.

إذا لم تكن الأمور واضحة، راجع مفهوم الـ Polymorphism في الـ OOP ثم اقرأ المقال مرة أخرى.

مفهوم الـ Dependency Injection

ربما لاحظت أن التعامل مع الـ Dependencies وتبدلها صار أسهل باستخدام الـ Interface والـ Polymorphism، لكن لا يزال تمرير الـ Class المناسب إلى جميع أجزاء المشروع أمرا متعبا، خاصة إذا كنت تستخدم Frameworks تقوم بإنشاء objects من بعض الكلاسات دون تدخل منك (مثل الـ Controller في معظم الـ Frameworks).

هنا تأتي فائدة الـ Dependency Injection، في مثالنا السابق كانت Stripe و Paypal تعتبر اعتماديات (Dependencies) يعتمد عليها المشروع حتى يعمل، الترجمة الحرفية لـ Dependency Injection هي (حقن الاعتماديات).

ابتداء من الآن، سأقوم بالإشارة إلى Dependency Injection بالاختصار DI

المصطلح قد يبدو معقدا، لكنه فعليا عبارة عن مكتبة بسيطة تقوم بإنشاء Object من Class نحدده لها وتمريره لأي interface عند الطلب.

على سبيل المثال، إذا حددنا لمكتبة الـ DI أننا نريد استخدام PaypalService عند طلب PaymentService Interface، في أي مكان نقوم فيه باستدعاء أو طلب الـ Interface، سنحصل على Object من PaypalService بشكل تلقائي.

الموضوع سيتضح أكثر بعد الأمثلة.

مثال على الـ DI باستخدام Laravel

سنقوم باستخدام ميزة الـ DI في لارافل، لكن بإمكانك بالطبع استخدامه مع أي Framework آخر، معظم أُطُر العمل الحديثة تدعم DI افتراضيا، في حال كنت تستخدم إطار عمل لا تتوفر فيه هذه الميزة، أو تستخدم PHP بدون إطار عمل، يمكنك استخدام مكتبة php-di.

داخل الـ AppServiceProvider في register() يمكنك عمل bind لخدمات الدفع التي قمنا بإنشائها:

1public function register()
2{
3    $this->app->bind(PaymentService::class, PaypalService::class);
4}

في هذه الحالة، عند طلب PaymentService ستقوم لارافل تلقائيا بتمرير Object من الكلاس PaypalService، لو فرضنا وجود هذه الـ Method في أي Controller:

1public function createInvoice(Request $request, PaymentService $paymentService)
2{
3    $invoice = new Invoice();
4    // ...
5
6    $paymentService->pay($invoice);
7}

فستقوم لارافل تلقائيا بتمرير Object من الكلاس PaypalService الى الباراميتر الثاني.

كما بامكانك طلب Object باستخدام DI عن طريق resolve() وهي function في Laravel تقوم بطلب Instance من أي Class، مثال:

1public function createInvoice(Request $request)
2{
3    $invoice = new Invoice();
4    // ...
5    $paymentService = resolve(PaymentService::class);
6    $paymentService->pay($invoice);
7}

وهذا سيؤدي إلى نفس النتيجة..

لاحظ أننا بسهولة، وبتعديل سطر واحد فقط في الكود، يمكننا استخدام الكلاس الخاص بـ Stripe بدلا من Paypal دون تعديل أي مكان آخر في المشروع:

1public function register()
2{
3    // $this->app->bind(PaymentService::class, PaypalService::class);
4    $this->app->bind(PaymentService::class, StripeService::class);
5}

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

 1public function register()
 2{
 3    if (auth()->check()) {
 4        $service = PaypalService::class;
 5        if (auth()->user()->inEurope()) {
 6            $service = StripeService::class;
 7        }
 8        $this->app->bind(PaymentService::class, $service);
 9    }
10}

اقرأ أيضا