Внедрение зависимостей в Android

Внедрение зависимостей (DI) — это метод, широко используемый в программировании и хорошо подходящий для разработки под Android. Следуя принципам DI, вы закладываете основу для хорошей архитектуры приложения.

Реализация внедрения зависимостей дает вам следующие преимущества:

  • Возможность повторного использования кода
  • Простота рефакторинга
  • Простота тестирования

Основы внедрения зависимостей

Прежде чем рассматривать внедрение зависимостей конкретно в Android, на этой странице представлен более общий обзор того, как работает внедрение зависимостей.

Что такое внедрение зависимостей?

Классы часто требуют ссылок на другие классы. Например, классу Car может потребоваться ссылка на класс Engine . Эти обязательные классы называются зависимостями , и в этом примере класс Car зависит от наличия экземпляра класса Engine для запуска.

У класса есть три способа получить необходимый ему объект:

  1. Класс создает необходимую ему зависимость. В приведенном выше примере Car создаст и инициализирует собственный экземпляр Engine .
  2. Возьмите его откуда-нибудь еще. Некоторые API-интерфейсы Android, такие как методы получения Context и getSystemService() , работают таким образом.
  3. Поставьте его в качестве параметра. Приложение может предоставить эти зависимости при создании класса или передать их функциям, которым нужна каждая зависимость. В приведенном выше примере конструктор Car получит в качестве параметра Engine .

Третий вариант — внедрение зависимостей! При таком подходе вы берете зависимости класса и предоставляете их вместо того, чтобы экземпляр класса получал их сам.

Вот пример. Без внедрения зависимостей представление Car , который создает в коде собственную зависимость Engine , выглядит следующим образом:

Котлин

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 в качестве параметра в своем конструкторе:

Котлин

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:

  • Внедрение конструктора . Это способ, описанный выше. Вы передаете зависимости класса его конструктору.

  • Внедрение поля (или внедрение сеттера) . Определенные классы платформы Android, такие как действия и фрагменты, создаются системой, поэтому внедрение конструктора невозможно. При внедрении полей зависимости создаются после создания класса. Код будет выглядеть так:

Котлин

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 .

Альтернативы внедрению зависимостей

Альтернативой внедрению зависимостей является использование локатора сервисов . Шаблон проектирования локатора сервисов также улучшает отделение классов от конкретных зависимостей. Вы создаете класс, известный как локатор сервисов , который создает и хранит зависимости, а затем предоставляет эти зависимости по требованию.

Котлин

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

Шаблон локатора сервисов отличается от внедрения зависимостей способом использования элементов. С помощью шаблона локатора сервисов классы имеют контроль и запрашивают внедрение объектов; при внедрении зависимостей приложение получает контроль и заранее внедряет необходимые объекты.

По сравнению с внедрением зависимостей:

  • Коллекция зависимостей, требуемых локатором сервисов, усложняет тестирование кода, поскольку все тесты должны взаимодействовать с одним и тем же глобальным локатором сервисов.

  • Зависимости кодируются в реализации класса, а не в поверхности API. В результате становится сложнее узнать, что нужно классу извне. В результате изменения в Car или зависимостях, доступных в локаторе служб, могут привести к сбоям во время выполнения или теста из-за сбоя ссылок.

  • Управлять временем жизни объектов сложнее, если вы хотите ограничиться чем-то другим, кроме времени жизни всего приложения.

Используйте Hilt в своем приложении для Android

Hilt — рекомендуемая Jetpack библиотека для внедрения зависимостей в Android. Hilt определяет стандартный способ реализации внедрения внедрения в вашем приложении, предоставляя контейнеры для каждого класса Android в вашем проекте и автоматически управляя их жизненными циклами.

Hilt построен на основе популярной библиотеки DI Dagger, чтобы получить преимущества от корректности времени компиляции, производительности во время выполнения, масштабируемости и поддержки Android Studio, которые обеспечивает Dagger.

Чтобы узнать больше о Hilt, см. «Внедрение зависимостей с помощью Hilt» .

Заключение

Внедрение зависимостей дает вашему приложению следующие преимущества:

  • Возможность повторного использования классов и разделение зависимостей: проще заменять реализации зависимости. Повторное использование кода улучшено благодаря инверсии управления, и классы больше не контролируют создание своих зависимостей, а вместо этого работают с любой конфигурацией.

  • Простота рефакторинга: зависимости становятся проверяемой частью поверхности API, поэтому их можно проверять во время создания объекта или во время компиляции, а не скрывать как детали реализации.

  • Простота тестирования: класс не управляет своими зависимостями, поэтому при его тестировании вы можете передавать разные реализации для проверки всех ваших разных случаев.

Чтобы полностью понять преимущества внедрения зависимостей, вам следует попробовать его вручную в своем приложении, как показано в разделе «Внедрение зависимостей вручную» .

Дополнительные ресурсы

Чтобы узнать больше о внедрении зависимостей, см. следующие дополнительные ресурсы.

Образцы