إدخال التبعية في Android

تقنية حقن التبعية (DI) هي تقنية تُستخدم على نطاق واسع في البرمجة ومناسبة تمامًا لتطوير Android. باتباع مبادئ DI، فإنك تضع الأساس لبنية التطبيق الجيدة.

يوفر لك تنفيذ حقن التبعية المزايا التالية:

  • إعادة استخدام الرمز
  • سهولة إعادة الهيكلة
  • سهولة الاختبار

أساسيات حقن التبعية

قبل تناول حقن التبعية في Android تحديدًا، توفر هذه الصفحة نظرة عامة أكثر عمومية حول طريقة عمل حقن التبعية.

ما هو حقن التبعية؟

تتطلب الصفوف غالبًا مراجع لفئات أخرى. على سبيل المثال، قد تحتاج فئة Car إلى مرجع لفئة Engine. تُسمى هذه الفئات المطلوبة تبعيات، وفي هذا المثال، تعتمد الفئة Car على وجود مثيل من الفئة Engine لتشغيله.

هناك ثلاث طرق لكي تحصل الفئة على كائن تحتاج إليه:

  1. تنشئ الفئة التبعية التي تحتاجها. في المثال أعلاه، ستنشئ دالة Car مثيل Engine الخاص بها وتعِدّها.
  2. استلِمها من مكان آخر. وتعمل بعض واجهات برمجة تطبيقات Android بهذه الطريقة، مثل رموز Context get وgetSystemService().
  3. يجب توفيرها كمَعلمة. ويمكن للتطبيق توفير هذه التبعيات عند إنشاء الفئة أو تضمينها في الدوال التي تحتاج إلى كل تبعية. في المثال أعلاه، ستتلقّى الدالة الإنشائية Car Engine كمَعلمة.

الخيار الثالث هو حقن التبعية! باستخدام هذا النهج، يمكنك أن تأخذ تبعيات الفئة وتقدمها بدلاً من الحصول على مثيل الفئة نفسها.

إليك مثالاً. بدون إدخال التبعية، سيبدو تمثيل Car الذي ينشئ تبعية Engine الخاصة به في الرمز على النحو التالي:

لغة Kotlin

class Car {

    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

جافا

class Car {

    private Engine engine = new Engine();

    public void start() {
        engine.start();
    }
}


class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}
فئة السيارة بدون حقن التبعية

وهذا ليس مثالاً على إدخال التبعية لأنّ الفئة Car تنشئ Engine الخاصة بها. قد يشكّل ذلك مشكلة للأسباب التالية:

  • إنّ الترميزَين Car وEngine مرتبطان ببعضهما البعض، حيث يستخدم مثيل Car نوعًا واحدًا من Engine، ولا يمكن استخدام فئات فرعية أو عمليات تنفيذ بديلة بسهولة. إذا كانت Car ستنشئ Engine الخاصة بها، يجب إنشاء نوعَين من Car بدلاً من إعادة استخدام السمة Car نفسها للمحرّكات من النوع Gas وElectric.

  • الاعتماد الشديد على Engine يجعل الاختبار أكثر صعوبة. Car يستخدم مثيلاً حقيقيًا من Engine، ما يمنعك من استخدام الاختبار المزدوج لتعديل Engine لحالات الاختبار المختلفة.

كيف تبدو التعليمة البرمجية مع حقن التبعية؟ وبدلاً من إنشاء Car لكائن Engine خاص به عند الإعداد، يتلقّى الكائن Engine كمَعلَمة في الدالة الإنشائية:

لغة Kotlin

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

جافا

class Car {

    private final Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
    }
}


class MyApp {
    public static void main(String[] args) {
        Engine engine = new Engine();
        Car car = new Car(engine);
        car.start();
    }
}
فئة سيارة باستخدام حقن التبعية

تستخدم الدالة main Car. بما أنّ Car يعتمد على Engine، ينشئ التطبيق مثيلًا لـ Engine ثم يستخدمه لإنشاء مثيل Car. في ما يلي فوائد هذا المنهج المستند إلى DI:

  • إعادة استخدام Car. يمكنك تمرير عمليات تنفيذ مختلفة من Engine إلى Car. على سبيل المثال، يمكنك تحديد فئة فرعية جديدة من Engine تسمى ElectricEngine وتريد أن تستخدمها Car. إذا كنت تستخدم DI، ما عليك سوى تمرير مثال من الفئة الفرعية ElectricEngine المعدَّلة، ويظل Car يعمل بدون أي تغييرات أخرى.

  • اختبار سهل لـ Car. يمكنك اجتياز الاختبار الزوجي لاختبار سيناريوهاتك المختلفة. على سبيل المثال، يمكنك إنشاء اختبار مزدوج من Engine يسمى FakeEngine، وإعداده لاختبارات مختلفة.

هناك طريقتان رئيسيتان لتنفيذ إدخال التبعية في Android:

  • حقن المنشئ: وهذه هي الطريقة الموضحة أعلاه. أنت تمرر تبعيات الفئة إلى الدالة الإنشائية لها.

  • حقن الحقل (أو حقن Setter): يُنشئ النظام مثيلاً لبعض فئات إطارات عمل Android، مثل الأنشطة والأجزاء، ولذلك لا يمكن تضمين دالة إنشاء. مع حقن الحقل، يتم إنشاء مثيل التبعيات بعد إنشاء الفئة. ستبدو التعليمة البرمجية كما يلي:

لغة Kotlin

class Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}

جافا

class Car {

    private Engine engine;

    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
    }
}

class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.setEngine(new Engine());
        car.start();
    }
}

حقن التبعية بشكل مبرمَج

في المثال السابق، قمت بإنشاء تبعيات الفئات المختلفة وتقديمها وإدارتها بنفسك، دون الاعتماد على مكتبة. ويُطلق على ذلك اسم إدخال التبعية يدويًا أو إدخال التبعية يدويًا. في المثال Car، كانت هناك تبعية واحدة فقط، ولكن المزيد من التبعيات والفئات يمكن أن تجعل إدخال التبعيات يدويًا أكثر مملة. يمثل حقن التبعية اليدوي أيضًا العديد من المشكلات:

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

  • عندما لا تتمكن من إنشاء تبعيات قبل تمريرها - على سبيل المثال عند استخدام التهيئة الكسولة أو تحديد نطاق الكائنات لتدفقات تطبيقك - فإنك تحتاج إلى كتابة والاحتفاظ بحاوية مخصصة (أو رسم بياني للتبعيات) التي تدير فترات عمر التبعيات في الذاكرة.

هناك مكتبات تحل هذه المشكلة عن طريق أتمتة عملية إنشاء التبعيات وتوفيرها. تندرج ضمن فئتين:

  • حلول قائمة على الانعكاس تربط التبعيات في وقت التشغيل.

  • حلول ثابتة تُنشئ التعليمات البرمجية لربط التبعيات في وقت التجميع.

Dagger هي مكتبة شائعة لتحميل التبعيات للغة Java وKotlin وAndroid، تتم صيانتها من خلال Google. يسهّل Dagger استخدام DI في تطبيقك من خلال إنشاء وإدارة الرسم البياني للتبعيات نيابة عنك. ويوفر ذلك تبعيات ثابتة بالكامل ووقت التجميع لمعالجة العديد من مشكلات التطوير والأداء للحلول القائمة على التفكير، مثل Guice.

بدائل حقن التبعية

بدلاً من إدخال التبعية، يمكنك استخدام محدِّد مواقع الخدمة. يعمل نمط تصميم محدد موقع الخدمة أيضًا على تحسين فصل الفئات عن التبعيات الملموسة. يمكنك إنشاء فئة تُعرف باسم محدِّد مواقع الخدمة الذي ينشئ التبعيات وتخزّنها ثم توفّر تلك التبعيات عند الطلب.

لغة Kotlin

object ServiceLocator {
    fun getEngine(): Engine = Engine()
}

class Car {
    private val engine = ServiceLocator.getEngine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

جافا

class ServiceLocator {

    private static ServiceLocator instance = null;

    private ServiceLocator() {}

    public static ServiceLocator getInstance() {
        if (instance == null) {
            synchronized(ServiceLocator.class) {
                instance = new ServiceLocator();
            }
        }
        return instance;
    }

    public Engine getEngine() {
        return new Engine();
    }
}

class Car {

    private Engine engine = ServiceLocator.getInstance().getEngine();

    public void start() {
        engine.start();
    }
}

class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}

ويختلف نمط محدد موقع الخدمة عن حقن التبعية في طريقة استهلاك العناصر. باستخدام نمط محدد موقع الخدمة، يكون لدى الفئات القدرة على التحكم وطلب إدخال العناصر؛ ومن خلال حقن التبعية، يتحكم التطبيق في الكائنات المطلوبة ويحقنها بشكل استباقي.

بالمقارنة مع حقن التبعية:

  • يجعل جمع التبعيات التي يتطلبها محدد موقع الخدمة من الصعب اختبار التعليمات البرمجية لأن جميع الاختبارات يجب أن تتفاعل مع نفس محدِّد مواقع الخدمة العالمية.

  • يتم ترميز التبعيات في تنفيذ الفئة، وليس في واجهة برمجة التطبيقات. ونتيجة لذلك، يصبح من الصعب معرفة ما يحتاجه الصف الدراسي من الخارج. ونتيجة لذلك، قد تؤدي التغييرات التي يتم إجراؤها على Car أو التبعيات المتاحة في "محدِّد مواقع الخدمة" إلى حدوث مشاكل في وقت التشغيل أو في الاختبار بسبب حدوث مشاكل في المراجع.

  • تكون إدارة الأعمار للكائنات أكثر صعوبة إذا كنت ترغب في تحديد نطاق أي شيء بخلاف عمر التطبيق بالكامل.

استخدام Hilt في تطبيق Android

Hilt هي مكتبة Jetpack المقترَحة لإدراج الاعتماد في Android. يحدد Hilt طريقة قياسية لتنفيذ أعمال DI في تطبيقك من خلال توفير حاويات لكل فئة Android في مشروعك وإدارة دورات الحياة تلقائيًا نيابة عنك.

تم تصميم Hilt على منصة Dagger الرائجة في مكتبة DI بالاستفادة من مدى صحة وقت التجميع، وأداء وقت التشغيل، وإمكانية التوسّع، ودعم "استوديو Android" الذي يوفّره Dagger.

للحصول على مزيد من المعلومات حول Hilt، يمكنك الاطّلاع على مقالة Someency Injection with Hilt (إدخال التبعية).

الخاتمة

يؤدي حقن التبعية إلى تزويد تطبيقك بالمزايا التالية:

  • قابلية استخدام الفئات وفصل التبعيات: من الأسهل تبديل تطبيقات التبعية. تم تحسين إعادة استخدام التعليمات البرمجية بسبب عكس التحكم، ولم تعد الفئات تتحكم في كيفية إنشاء تبعياتها، ولكن بدلاً من ذلك تعمل مع أي تهيئة.

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

  • سهولة الاختبار: لا تدير الفئة تبعياتها، لذا عند اختبارها، يمكنك تمرير عمليات تنفيذ مختلفة لاختبار جميع حالاتك المختلفة.

للتعرّف بشكل كامل على فوائد إضافة الاعتمادية، عليك تجربتها يدويًا في تطبيقك كما هو موضَّح في إدخال الاعتمادية يدويًا.

مراجع إضافية

لمعرفة المزيد حول حقن التبعية، راجع الموارد الإضافية التالية.

عيّنات