February 10, 2020

Dagger 2 для начинающих Android разработчиков. Внедрение зависимостей

Это вторая статья цикла «Dagger 2 для начинающих Android разработчиков». Первую часть можно прочесть здесь.

Ранее в цикле статей

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

Что такое внедрение зависимостей (dependency injection, DI)

Ранее мы поняли, что такое зависимость и как она влияет на проект. Сейчас разберем, что такое внедрение зависимостей.

Перед тем как разбирать внедрение зависимостей, нам нужно понять, как избежать ловушку, в которой мы будем окружены зависимостями. Проблема сильных связей (hard dependency) как Белые Ходоки (White Walkers). Мы не должны присоединиться к их армии, напротив, нужно найти путь, чтобы победить их.

Стратегия решения проблемы сильных связей (hard dependency) или проблемы Белых Ходоков

Нам нужен четкий план для решения или предотвращения проблемы сильных связей. Эта стратегия или план называется внедрение зависимостей (dependency injection, DI). Другими словами, чтобы убить Белого Ходока, вам нужно сжечь его или использовать оружие из драконьего стекла. Аналогично, чтобы избежать сильных связей, необходимо использовать внедрение зависимостей.

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

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

Внедрение зависимостей — это метод, при котором один объект предоставляет зависимости другого объекта. Зависимость (dependency) – это объект, который мы можем использовать (сервис). Внедрение (инъекция, injection) – это передача зависимости зависимому объекту (клиенту), который будет данной зависимостью пользоваться. Сервис — это часть состояния клиента. Передать сервис клиенту, вместо того чтобы позволить клиенту создать или найти сервис, – базовое требование шаблона проектирования «Внедрение зависимости» (DI).

Сравним это с Игрой Престолов. Серсея готовится к большой войне. Вместо того чтобы сжигать все деньги в своем королевстве, она пытается получить кредит у железного банка Браавоса. В этой ситуации деньги — это зависимость. Деньги нужны всем домам, чтобы вести войну. Таким образом внешний объект (железный банк) будет внедрять зависимость (деньги) зависимым объектам (домам).

Другими словами, внедрение зависимостей основывается на концепции инверсии контроля, которая говорит о том, что класс должен получать свои зависимости извне. Говоря просто, ни один класс не должен создавать экземпляр другого класса, а должен получать все экземпляры из класса конфигурации.

Пример

Хватить говорить. Давайте разбирать код. Рассмотрим небольшой пример с двумя сценариями. В первом сценарии создадим несколько классов с сильными связями (hard dependencies), то есть без использования внедрения зависимостей. Затем, следуя шаблону «Внедрение зависимости», мы избавимся от сильных связей.

Сценарий 1. Без использования внедрения зависимостей

Проблема: Битва бастардов — Старки (Starks) и Болтоны (Boltons) готовятся к войне, чтобы захватить Север. Нужно их подготовить и вывести на войну.

Поскольку пример может включать много домов, создадим общий интерфейс House, включим в него методы prepareForWar() и reportForWar().

interface House {
    fun prepareForWar()
    fun reportForWar()
}

Далее создадим классы для домов Starks и Boltons. Классы будут реализовать интерфейс House.

class Starks : House {

    override fun prepareForWar() {
        //что-то происходит
        println("${this::class.simpleName} prepared for war")
    }

    override fun reportForWar() {
        //что-то происходит
        println("${this::class.simpleName} reporting...")
    }
}
class Boltons : House {

    override fun prepareForWar() {
        //что-то происходит
        println("${this::class.simpleName} prepared for war");
    }

    override fun reportForWar() {
        //что-то происходит
        println("${this::class.simpleName} reporting..");
    }
}

Заметка: this::class.simpleName просто возвращает имя класса.

Далее нужно привести оба дома к войне. Создадим класс War и попросим оба дома подготовиться к войне и сообщить о ней.

class War {

    private val starks = Starks()
    private val boltons = Boltons()

    init {
        starks.prepareForWar()
        starks.reportForWar()
        boltons.prepareForWar()
        boltons.reportForWar()
    }
}

Анализируем War

Рассмотрим класс War. Для работы ему необходимы два класса Starks и Boltons. Эти классы создаются внутри класса War, готовятся к войне и сообщают о ней.

В хорошо спроектированном объектно-ориентированном приложении у каждого объекта минимальное количество обязанностей, объекты опираются на другие для выполнения большей части работы. В данном примере класс War зависит от Starks и Boltons. Это зависимости класса War. Без них War работать не будет. Перед тем как класс начнет выполнять реальные функции, все его зависимости должны быть удовлетворены каким-либо образом. Зависимости класса War в этом примере удовлетворяются путем создания экземпляров классов в конструкторе.

Хотя вариант с созданием зависимостей внутри конструктора класса достаточно хорошо работает в небольших приложениях — он привносит недостатки по мере роста проекта.

Во-первых, класс становится довольно негибким. Если приложение должно работать на нескольких платформах или в нескольких режимах, например, необходимо заменить класс Boltonsна другой или разделить его на несколько объектов. Сделать это становится не так просто.

Во-вторых, невозможно изолированно протестировать эти классы. Создание экземпляра War автоматически создает два других объекта, которые, в конечном итоге, будут тестироваться вместе с классом War. Это может стать проблемой, если один из объектов зависит от дорогого внешнего ресурса, например, Allies (союзники) или один из объектов сам зависит от множества других.

Как упоминалось ранее — борьба с сильными связями (hard dependencies) похожа на борьбу с Белыми Ходоками без надлежащего оружия.

Сценарий 2. Использование драконьего стекла

Пришло время воспользоваться драконьим стеклом и уничтожить Белых Ходоков. Да, мы будем использовать внедрение зависимостей, чтобы устранить сильные связи.

Вспомним идею внедрения зависимостей. Класс не должен создавать другие классы. Вместо этого он должен получать зависимости снаружи. Давайте получим зависимости Boltons и Starks снаружи, через конструктор класса War.

class War(
    private val starks: Starks,
    private val boltons: Boltons
) {

    fun prepare() {
        starks.prepareForWar()
        boltons.prepareForWar()
    }

    fun report() {
        starks.reportForWar()
        boltons.reportForWar()
    }
}

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

Класс War должен знать не только о том, как выполнить определенную задачу, но и о том, где искать классы, в которых он нуждается для выполнения своих задач. Если мы предоставим всё необходимое для работы нашему классу снаружи, то избавимся от ранее рассмотренных проблем. Класс легко сможет работать с любыми экземплярами других классов, которые нужны ему для выполнения задач, и будет просто тестироваться в изоляции от них. В приложении, использующем внедрение зависимостей, объекты никогда не будут искать зависимости или создавать их внутри себя. Все зависимости предоставляются ему или внедряются в него готовыми к использованию.

В какой-то момент, конечно, кто-то должен создать экземпляры классов зависимостей и предоставить их объектам, которые в этом нуждаются. Обычно такая работа выполняется в точке входа в приложение. В обычном Kotlin приложении, например, такой код можно найти внутри метода main(), как показано ниже. В Android это обычно делается в методе onCreate() внутри Activity.

fun main() {
    val starks = Starks()
    val boltons = Boltons()

    val war = War(starks, boltons)
    war.prepare()
    war.report()
}

В классе BattleOfBastards мы создаем зависимости Boltons и Starks и внедряем из через конструктор в класс War. Зависимый класс War зависит от зависимостей Boltons и Starks.

Время по достоинству оценить себя и отпраздновать. Да, мы уничтожили Белых Ходоков (сильные связи)! Надеюсь, вы поняли концепцию того, что мы пытаемся разобрать.

Резюме

В этой статье мы рассмотрели зависимости и внедрение зависимостей в деталях. Ни один класс не должен создавать экземпляр другого класса, а должен получать все экземпляры снаружи.

Мы рассмотрели пример с сильными связями (hard dependencies), создав сценарий битвы бастардов. Затем мы попытались устранить сильные связи с помощью внедрения зависимостей.

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

Что дальше?

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

Источники:
Dagger 2 для начинающих Android разработчиков. Внедрение зависимостей. Часть 1
Dagger 2 для начинающих Android разработчиков. Внедрение зависимостей. Часть 2