Дружнє введення в Dagger 2. Частина 2

Використовуємо модулі, щоб визначити, як повинні створюватися об'єкти

У попередній статті з цієї серії ми розглянули, як 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 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.