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

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

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

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

أساسيات إدخال التبعية

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

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

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

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

  1. تنشئ الفئة التبعية التي تحتاجها. في المثال أعلاه، سينشئ Car مثيلاً خاصًا به من Engine ويضبطه.
  2. يمكنك جلب الجهاز من مكان آخر. وتعمل بعض واجهات برمجة تطبيقات Android بهذه الطريقة، مثل رموز Context و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()
}

Java

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()
}

Java

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:

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

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

Kotlin

class Car {
    lateinit var engine: Engine

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

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

Java

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()
}

Java

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، يُرجى الاطّلاع على مقالة Credential Injection باستخدام Hilt.

الخاتمة

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

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

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

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

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

مراجع إضافية

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

عيّنات