Используем модули, чтобы указать, как должны создаваться объекты
В предыдущей статье из этой серии мы рассмотрели, как Dagger 2 избавляет нас от рутины написания инициализирующего кода путем внедрения зависимостей.
Если помните, мы создали интерфейс, позволяющий фреймворку узнать, объекты каких классов требуются нашему методу main, а Dagger автоматически сгенерировал конкретный класс, способный инициализировать экземпляры этих классов за нас. Мы нигде не указывали, как именно создавать эти объекты или их зависимости. Поскольку все наши классы были конкретными и помечены соответстующими аннотациями, это не создавало проблем: Dagger из аннотаций мог сделать вывод, чьи конструкторы необходимы для создания экземпляра данного класса.
Однако, чаще всего классы зависят не от конкретных, а от абстрактных классов и интерфейсов, не имеющих конструкторов, которые мог бы вызвать Dagger. Иногда изменить исходник класса для включения аннотации вообще не вариант. Еще бывает, что создание объекта требует большего количества действий, чем просто вызов конструктора. Во всех этих случаях автоматического поведения Dagger'а недостаточно и фреймворку требуется наша помощь.
В сегодняшней статье мы увидим, как предоставить Dagger'у дополнительные инструкции по созданию объектов посредством модулей (module). Модули взаимозаменяемы и могут быть использованы и в других проектах. Плюс они могут принимать аргументы в рантайме, что делает их еще более гибкими.
Пример
Дабы проиллюстрировать описанную выше ситуацию, давайте вернемся к первому примеру из предыдущей статьи, где у нас было всего 3 класса: WeatherReporter и 2 его зависимости, LocationManager и WeatherService. Обе зависимости были конкретными классами. На практике этого может и не случиться.
Давайте предположим, что WeatherService — это интерфейс, и у нас есть еще один класс, скажем, YahooWeather, который имплементирует этот интерфейс:
package com.example;
public interface WeatherService {
}
package com.example;
import javax.inject.Inject;
public class YahooWeather implements WeatherService {
@Inject
public YahooWeather() {
}
}
Если мы снова попытаемся скомпилировать проект, Dagger выдаст ошибку и скажет, что он не может найти provider для WeatherService.
Когда класс конкретный и у него есть аннотированный конструктор, Dagger может автоматически сгенерировать provider для этого класса. Однако поскольку WeatherService — интерфейс, мы должны предоставить Dagger'у больше информации.
Что такое модули?
Модули — это классы, способные создавать экземпляры определенных классов. Например, следующий модуль способен создавать по запросу объекты WeatherService, создавая экземпляр класса YahooWeather.
@Module
public class YahooWeatherModule {
@Provides
WeatherService provideWeatherService() {
return new YahooWeather();
}
}
Модули должны быть помечены аннотацией @Module. Некоторые из их методов, известные также как provider-методы, помечены аннотацией Provides для указания, что они могут предоставлять по запросу экземпляры определенного класса. Имена методов значения не имеют: Dagger смотрит лишь на сигнатуры.
Используя модули, мы совершенствуем Dagger'овские возможности создания объектов и разрешения зависимостей. Раньше в качестве зависимостей могли использоваться только конкретные классы с аннотированными конструкторами, а теперь, с модулем, любой класс может зависеть от интерфейса WeatherService. Нам осталось только подсоединить этот модуль к компоненту, который используется в точке входа нашего приложения:
@Component(modules = {YahooWeatherModule.class})
interface AppComponent {
WeatherReporter getWeatherReporter();
}
Проект снова компилируется. Каждый экземпляр WeatherReporter, созданный методом getWeatherReporter, создает экземпляр YahooWeather.
Мы можем создать один большой модуль, знающий о том, как создать любой абстрактный класс или интерфейс в приложении, или же множество небольших модулей, занятых созданием непосредственно связанных друг с другом классов. В этой серии статей мы будем использовать второй подход.
Как подменять модули
Одно из самых интересных свойств модуля — это то, что он легко может быть заменен на любой другой. Это позволяет нам располагать множественными реализациями интерфейса в проекте и легко переключаться между ними.
Положим, у нас есть еще один класс, WeatherChannel, также реализующий WeatherService. Если мы захотим использовать этот класс в WeatherReporter взамен YahooWeather, мы можем написать новый модуль WeatherChannelModule и подставить в компонент именно его.
@Module
public class WeatherChannelModule {
@Provides
WeatherService provideWeatherService() {
return new WeatherChannel();
}
}
@Component(modules = {WeatherChannelModule.class})
public interface AppComponent {
WeatherReporter getWeatherReporter();
}
Замена модулей полезна, например, при написании интеграционных и функциональных тестов. Можно определить другой компонент, практически идентичный первому, но использующий несколько другие модули. Мы еще вернемся более подробно к этому вопросу в следующей статье.
Если мы попробуем подсоединить к компоненту два различных модуля, возвращающих один и тот же тип, Dagger выдаст ошибку компиляции, сообщив, что тип привязан множество раз (type is bound multiple times). Например, так сделать не выйдет:
@Component(modules = {WeatherChannelModule.class, YahooWeatherModule.class})
public interface AppComponent {
WeatherReporter getWeatherReporter();
}
Кроме того, поскольку Dagger 2 генерирует компоненты во время компиляции, модули не получится заменять в рантайме. Однако наша точка входа может иметь несколько компонентов в своем распоряжении и решать, от какого из них получать объекты, основываясь на конкретных условиях во время выполнения.
Создание более сложных объектов
Теперь, когда мы знаем, как написать простенький модуль и что это вообще такое, приступим к написанию модулей, создающих более сложные объекты. Например, экземпляры классов, определенных третьей стороной (которые не могут быть изменены), имеющие зависимости или требующие конфигурирования.
Создание экземпляров сторонних (third-party) классов
Иногда изменить класс и добавить в него аннотацию невозможно. Например, класс является частью фреймворка или сторонней библиотеки. Также достаточно часто сторонние классы имеют сомнительный дизайн и не облегчают применение DI-фреймворков.
Предположим, наш LocationManager зависит от GpsSensor. А этот класс предоставлен компанией «Рога и копыта» и не может быть изменен. Усложним ситуацию еще больше: конструктор класса инициализирует его не полностью. После создания экземпляра класса, прежде чем его использовать, мы обязаны вызвать еще методы, такие как calibrate. Ниже исходники LocationManager и GpsSensor.
public class GpsSensor {
public GpsSensor() {
}
public void calibrate() {
}
}
public class LocationManager {
private final GpsSensor gps;
@Inject
public LocationManager(GpsSensor gps) {
this.gps = gps;
}
}
Обратите внимание, в GpsSensor нет аннотаций. Поскольку продукты известной компании просто работают, они не нуждаются во внедрении зависимостей или тестах.
Нам хотелось бы избежать вызова метода calibrate в конструкторе LocationManager, поскольку настройка GpsSensor'а — это не его обязанность. В идеале все принимаемые зависимости уже должны быть готовы к использованию. Кроме того, этот экземпляр GpsSensor может быть использован во многих местах, а «Рога и копыта» предупреждают, что множественный вызов calibrate приводит к крэшу.
Чтобы в дальнейшем использовать GpsSensor, не опасаясь последствий, мы можем написать модуль, чьей единственной обязанностью будет создать и настроить этот объект по запросу. Таким образом, любой класс может зависеть от GpsSensor, не беспокоясь о его инициализации.
@Module
public class GpsSensorModule {
@Provides
GpsSensor provideGpsSensor() {
GpsSensor gps = new GpsSensor();
gps.calibrate();
return gps;
}
}
Создание объектов с зависимостями
Иногда нашим модулям требуется создавать объекты с зависимостями. Создавать эти зависимости или заниматься их поиском — явно не обязанность модуля. Как и любой другой класс, он ожидает, что зависимости будут ему предоставлены в готовом виде.
Предположим, например, что классу YahooWeather для работы требуется WebSocket. Взглянем на код.
public class WebSocket {
@Inject
public WebSocket() {
}
}
public class YahooWeather implements WeatherService {
private final WebSocket socket;
@Inject
public YahooWeather(WebSocket socket) {
this.socket = socket;
}
}
Поскольку конструктор YahooWeather требует теперь передачи праметра, нам требуется изменить YahooWeatherModule. Необходимо так или иначе получить экземпляр WebSocket для вызова конструктора.
Вместо создания зависимости прямо внутри модуля, что свело бы на нет все преимущества DI, мы можем просто изменить сигнатуру provider'а. А Dagger уже сам позаботится о том, чтобы создать экземпляр WebSocket.
@Module
public class YahooWeatherModule {
@Provides
WeatherService provideWeatherService(WebSocket socket) {
return new YahooWeather(socket);
}
}
Создание объектов, требующих настройки конфигурации
Иногда модулю требуется создать объект, который должен быть каким-то образом сконфигурирован, а эта конфигурация требует информации, доступной только в рантайме.
Давайте представим, что конструктору YahooWeather требуется еще и ключ API.
public class YahooWeather implements WeatherService {
private final WebSocket socket;
private final String key;
public YahooWeather(String key, WebSocket socket) {
this.key = key;
this.socket = socket;
}
}
Как видите, мы удалили аннотацию Inject. Поскольку за создание YahooWeather теперь отвечает наш модуль, Dagger'у о конструкторе знать не требуется. Точно также он не знает, как автоматически внедрять параметр String (хотя это несложно сделать, как — увидим в будущей статье).
Если ключ API — константа, доступная после компиляции, например, в классе BuildConfig, возможно такое решение:
@Module
public class YahooWeatherModule {
@Provides
WeatherService provideWeatherService(WebSocket socket) {
return new YahooWeather(BuildConfig.YAHOO_API_KEY, socket);
}
}
Учтите, однако, что ключ API доступен только в рантайме. Например, он может быть аргументом командной строки. Эта информация может быть предоставлена модулю, как и любая другая зависисмость, через конструктор.
@Module
public class YahooWeatherModule {
private final String key;
public YahooWeatherModule(String key) {
this.key = key;
}
@Provides
WeatherService provideWeatherService(WebSocket socket) {
return new YahooWeather(key, socket);
}
}
Это немного осложняет жизнь Dagger'у, поскольку он теперь не в курсе, как создать такой модуль. Когда у модулей есть конструкторы без аргументов, Dagger легко может их инициализировать и использовать. В нашем же случае мы должны сами инициализировать наш модуль и передать его в пользование Dagger'у. Это можно сделать в точке входа в наше приложение:
public class Application {
public static void main(String args[]) {
String apiKey = args[0];
YahooWeatherModule yahoo = new YahooWeatherModule(apiKey);
AppComponent component = DaggerAppComponent.builder()
.yahooWeatherModule(yahoo)
.build();
WeatherReporter reporter = component.getWeatherReporter();
reporter.report();
}
}
В строках 3-4 мы получаем ключ API из аргументов командной строки и сами создаем экземпляр модуля. В строках 5-7 мы просим Dagger создать новый компонент, используя свежесозданный экземпляр модуля. Вместо вызова метода create, мы обращаемся к builder'у, передаем наш экземпляр модуля и наконец вызываем build.
Будь подобных модулей больше, нам пришлось бы подобным образом инициализировать и их до вызова build.
Заключение
В сегодняшней статье мы рассмотрели, как можно управлять созданием объектов посредством модулей. Исходники для статьи доступны на Github.
В следующей статье мы рассмотрим, как указать, что определенные зависимости могут быть использованы множестом объектов.
Прим. от переводчика. Пока автор не написал продолжение, всем заинтересованным предлагаю продолжить знакомство с Dagger'ом по замечательной серии постов от xoxol_89 (ч.1, ч.2)
Комментарии (0)