Дружественное введение в Dagger 2. Часть 1

Что такое внедрение зависимостей, что представляет собой Dagger и как он может пригодиться нам в написании более чистого и простого в тестировании кода.


Дисклеймер от переводчика. Данный перевод выполнен в целях самообразования, а на Хабре выложен в предположении, что многим начинающим Android-девелоперам, которым, как и мне, не довелось родиться Java-говорящим разработчиком в пятом поколении, достаточно сложно разобраться в конечных продуктах многолетних наслоений концепций и методов разработки. Эта серия статей — отличный пример того, как нужно объяснять сложные вещи, и, надеюсь, Вам она понравится не меньше, чем мне. Обо всех замеченных ошибках и неточностях прошу сообщать в личку.

Внедрение зависимостей (Dependency injection, DI) — великолепная техника, упрощающая покрытие приложения тестами, а Dagger 2 — один из самых популярных Java/Android фреймворков, предназначенных для этой цели. При этом большинство вводных курсов по Dagger 2 исходят из предположения, что читатель уже хорошо знаком с DI и его достаточно сложной терминологией, затрудняющей вхождение для новичков.

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

Дабы не растекаться мыслью по древу, я умышленно оставляю за бортом историю DI-фреймворков, ровно как и их бесчисленные разновидности. Начнем мы с внедрения в конструктор класса, а на остальные варианты обратим взор по мере продвижения.

Итак, что же такое внедрение зависимостей?


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

    private final WeatherService weatherService;

    private final LocationManager locationManager;

    public WeatherReporter() {
        weatherService = new WeatherService();
        locationManager = new LocationManager();
    }

    public void report() {
        // locationManager.getCurrentLocation()
        // weatherService.getTemperature(location)
        // print(temperature)
    }
}

Некоторые методы намеренно опущены, поскольку в данном случае неважны. Заметьте, что WeatherReporter'у для выполнения собственной работы необходимы 2 объекта: LocationManager, опеределяющий местоположение пользователя, и WeatherService, выдающий температуру по заданным координатам.

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

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

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

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

public class WeatherReporter {

    private final WeatherService weatherService;

    private final LocationManager locationManager;

    public WeatherReporter(WeatherService weatherService,
                           LocationManager locationManager) {
        this.weatherService = weatherService;
        this.locationManager = locationManager;
    }

    public void report() {
        // locationManager.getCurrentLocation()
        // weatherService.getTemperature(location)
        // print(temperature)
    }
}

Этот подход называется внедрение зависимостей. В приложении, которое полагается на DI, объектам не приходится «рыскать вокруг» в поисках зависимостей или самим их создавать. Все зависимости, которые им предоставляются (внедряются), уже готовы к использованию.

Построение графа


Разумеется, в какой-то момент кто-нибудь должен проинициализировать все зависимости и предоставить их тем объектам, которые в них нуждаются. Этот этап, именуемый построением графа зависимостей, обычно выполняется в точке входа в приложение. В десктопном приложении, например, этот код располагается внутри метода main, как в примере ниже. В Андроид-приложении это можно сделать внутри метода onCreate activity.
public class Application {
    public static void main(String args[]) {
        WeatherService ws = new WeatherService();
        LocationManager lm = new LocationManager();
        WeatherReporter reporter = new WeatherReporter(ws, lm);
        reporter.report();
    }
}

Если проект прост, как в нашем предыдущем примере, инициализация и внедрение нескольких зависимостей в методе main оправданы. Однако большинство проектов состоит из множества классов со своими зависимостями, которые должны быть разрешены. Инициализация и связывание всего этого вместе требует написания большого количества кода. Даже хуже, этот код будет регулярно изменяться при добавлении нового класса в приложение или добавлении новой зависимости в существующий класс.

Чтобы проиллюстрировать эту проблему, давайте превратим наш пример в более реалистичный. На практике классу WeatherService понадобится, скажем, WebSocket для коммуникации с сетью. LocationManager'у понадобится GPSProvider для взаимодействия с железом. Помимо прочего, большинству классов понадобится Logger для вывода отладочной информации в консоль. Модифицированный метод main выглядит теперь так:

public class Application {
    public static void main(String args[]) {
        Logger logger = new Logger();
        WebSocket socket = new WebSocket();
        GPSProvider gps = new GPSProvider();
        WeatherService ws = new WeatherService(logger, socket);
        LocationManager lm = new LocationManager(logger, gps);
        WeatherReporter reporter = new WeatherReporter(logger, ws, lm);
        reporter.report();
    }
}

Очень быстро точка входа в наше приложение начала раздуваться от обилия инициализирующего кода. Чтобы создать единственный реально необходимый нам объект WeatherReporter, нам приходится вручную инициализировать множество других объектов. По мере того как приложение будет разрастаться и обрастать классами, метод main также продолжит разбухать, пока однажды не станет совершенно неподдерживаемым.

Как может помочь Dagger 2?


Dagger 2 — это инструмент с открытым исходным кодом, генерирующий большую часть инициализирующего кода за нас, основываясь всего на нескольких аннотациях. При использовании Dagger 2 точка входа в наше приложение может быть написана всего в несколько строк кода, независимо от того, сколько классов у нас есть и сколько в них присутствует зависимостей. Ниже новый метод main для нашего примера с использованием Dagger.
public class Application {
    public static void main(String args[]) {
        AppComponent component = DaggerAppComponent.create();
        WeatherReporter reporter = component.getWeatherReporter();
        reporter.report();
    }
}

Заметьте, что мы не обязаны теперь писать код для разрешения зависимостей или указывать как эти зависимости переплетаются между собой. Это уже сделано за нас. Класс DaggerAppComponent, автоматически генерируемый во время компиляции проекта, достаточно умен, чтобы знать, что классу WeatherReporter нужны Logger, LocationManager и WeatherService, которому в свою очередь нужны GPSProvider и WebSocket. При вызове метода getWeatherReporter он создаст все эти объекты в верной последовательности, создаст между ними связи и вернет только то, что нам требовалось.

Чтобы Dagger заработал, мы должны выполнить несколько шагов. Во-первых, добавить аннотацию в конструктор каждого класса, о котором Dagger должен знать. Ниже пример того, как сделать это для одного конкретного класса, но точно так же необходимо аннотировать все классы, которым требуются какие-то зависимости или которые сами выступают зависимостями для других классов.

public class GPSProvider {
    @Inject
    public GPSProvider() {
        // ...
    }
}

Если вы переживаете по поводу загрязнения приложения специфичным для Dagger’а кодом, вам будет приятно узнать, что аннотация Inject стандартизирована (JSR 330) и с ней работают многие другие инструменты, навроде Spring или Guice. Соответственно, переключение на другой DI-фреймворк не потребует изменения множества классов в приложении.

На следующем шаге нам необходимо создать интерфейс с аннотацией Component, объявляющий методы, которые будут возвращать необходимые нам объекты. Необязательно объявлять здесь методы для каждого класса в нашем проекте, обязательны только методы для классов, непосредственно используемых в точке входа в приложение. В нашем примере методу main нужен только WeatherReporter, так что объявляем в интерфейсе только один метод.

@Component
public interface AppComponent {
    WeatherReporter getWeatherReporter();
}

Все, что осталось — интегрировать Dagger с нашей системой сборки. При использовании Gradle просто добавляем новые зависимости в build.gradle.
plugins {
    id "net.ltgt.apt" version "0.7"
}

dependencies {
    apt     'com.google.dagger:dagger-compiler:2.6'
    compile 'com.google.dagger:dagger:2.6'
}

Все, проект может быть скомпилирован и выполнен. Если хотите попробовать сделать это сами, исходники для нашего примера доступны на Github.

В качестве заключения. Обратите внимание: мы все еще можем использовать любой класс без Dagger, как и прежде. Например, мы все еще можем инициализировать Logger вручную посредством оператора new, скажем, в юнит-тесте. Dagger не изменяет поведение языка, он просто генерирует удобные классы, которые выполняют инициализацию объектов за нас.

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

Что почитать


Если внедрение зависимостей кажется вам бесполезной техникой или вы хотите увидеть больше примеров, когда DI упрощает тестирование, обратите внимание на потрясающее руководство Writing Testable Code (pdf) авторства Miško Hevery, Russ Ruffer и Jonathan Wolter.

Если вас больше интересует теория внедрения зависмостей и все его разновидности, рекомендую эссе Inversion of Control Containers and the Dependency Injection pattern (перевод на русский — ч.1, ч.2) Мартина Фаулера.

To be continued…

Оригинал статьи

Комментарии (0)

© Habrahabr.ru