Dependency Injection by Dagger 2
Як впровадження залежностей може ускладнитися?
Створення екземплярів і впровадження невеликої кількості залежностей вручну, через точку входу (main() або onCreate() метод) у програму дуже розумно. Однак, у багатьох проєктах є безліч класів, у кожного з яких є різні залежності, які потрібно задовольнити. Для створення екземплярів і підключення всього разом потрібна велика кількість коду. Ще гірше те, що цей код постійно змінюватиметься щоразу під час додавання нових класів до застосунку та під час зміни наявних класів для того, щоб впровадити нові залежності.
Щоб проілюструвати описану проблему, давайте трохи ускладнимо наш приклад. Під час війни, у битві бастардів (BattleOfBastards) імовірно знадобиться допомога союзників (Allies). Також залізний банк (IronBank) фінансуватиме будинки. Змінений головний метод матиме такий вигляд:
public class BattleOfBastards {
public static void main(String[] args){
IronBank bank = new IronBank();
Allies allies = new Allies(bank);
Starks starks = new Starks(allies, bank);
Boltons boltons = new Boltons(allies, bank);
War war = new War(starks, boltons);
war.prepare();
war.report();
}
}
Дуже швидко точка входу в додаток буде наповнена величезною кількістю коду для ініціалізації всіх залежностей. Для створення одного класу, з яким ми будемо працювати, потрібно ініціалізувати кілька інших. У міру зростання застосунку і додавання в нього нових класів, точка входу в застосунок роздуватиметься і зрештою цей метод стане дуже складно підтримувати.
Dagger 2 - це один із фреймворків із відкритим вихідним кодом для впровадження залежностей (далі використовуватиму DI, від Dependency Injection), який генерує велику кількість шаблонного коду за вас. Чому він кращий за інші? Наразі це єдиний DI фреймворк, який генерує повністю відстежуваний Java код, що імітує той код, який ви могли написати вручну. Це означає, що в побудові графа залежностей немає ніякої магії. Dagger 2 менш динамічний, ніж інші (у ньому не використовується рефлексія), але простота і продуктивність згенерованого коду перебувають на тому самому рівні, що й у написаного вручну. Коротко, Dagger 2 генерує весь шаблонний код для впровадження залежностей за вас.
Ручне управління впровадженням залежностями - це як видобуток драконячого скла. Спочатку ви отримуєте дозвіл від королеви драконів, потім куєте зброю і тільки потім йдете воювати з Білими Ходоками (проблемами сильних зв'язків). Dagger 2 схожий на валірійський меч - він був створений майстрами, і все, що вам потрібно - просто використовувати його.
media
Розуміння обробників анотацій (Annotation Processors)
#Аннотації
Анотації - це вид метаданих, який може бути пов'язаний з класами, методами, полями і навіть іншими анотаціями. Анотації використовуються в Java для надання додаткової інформації, як альтернатива XML або маркерним інтерфейсам (порожні інтерфейси). До анотацій можна отримати доступ і в процесі виконання програми (runtime) через механізм рефлексії.
#Обробники анотацій (Annotation Processors)
Обробники анотацій - це генератори коду, які приховують від вас шаблонний код, створюючи його за вас під час компіляції. Поки ці дії виконуються під час компіляції, жодного негативного впливу на продуктивність немає.
#Чому я маю знати про обробники анотацій?
Dagger 2 використовує їх. Таким чином, можна відстежити весь згенерований код під час компіляції. Отже, немає погіршення продуктивності, а помилки легко відстежуються.
#Приклади
Ви часто бачите в класах анотацію @Override. Якщо ви використовували Butterknife, @BindView - це теж анотація, яка приховує за собою деякі метадані, що допомагають генерувати код.
Анотації Dagger 2
Розглянемо деякі анотації Dagger 2 перед тим, як будемо його використовувати. Зараз зосередимося на двох - @Inject і @Component.
@Inject
Це найбільш важлива анотація. JSR-330 визначає цю анотацію як позначку для залежностей, які мають бути надані фреймворком для впровадження залежностей.
- Впровадження з використанням конструктора (Constructor Injection) - використовується з конструктором класу.
- Впровадження поля (Field Injection) - використовується з полями класу.
- Впровадження з використанням методу (Method Injection) - використовується з методами
public class Starks {
/**
* Пояснення різного використання
* анотації Inject у Dagger
**/
// Feild injection
@Inject
Allies allies;
// Constructor injection
@Inject
public Starks() {
// щось відбувається
}
//Method injection
@Inject
private void prepareForWar() {
// щось відбувається
}
}
Іншими словами, анотація @Inject повідомить Dagger про те, які залежності повинні бути надані залежному об'єкту. Це як агенти залізного банку, які ведуть переговори з будинками і визначають суму кредиту, яку можуть надати дому.
@Component
Ця анотація використовується для інтерфейсу, який об'єднає всі частини процесу впровадження залежностей. Під час використання цієї анотації ми визначаємо, з яких модулів або інших компонентів будуть братися залежності. Також тут можна визначити, які залежності буде видно відкрито (можуть бути впроваджені) і де компонент може впроваджувати об'єкти. @Component, загалом, щось на кшталт моста між @Module (розглянемо цю анотацію пізніше) і @Inject.
Іншими словами, ця анотація наче агент залізного банку, який відповідає за затвердження кредиту та переказ грошей на відповідний рахунок.
Убити Білих Ходаків валірійським мечем
Давайте використаємо Dagger 2 для прикладу з битвою бастардів. У цьому прикладі потрібні дві залежності для класу War - Starks і Boltons.
Налаштування Dagger 2
Для налаштування Dagger 2 в IntelliJ Idea використовуйте build.gradle файл із гілки мого проєкту. Також переконайтеся в тому, що обробку анотацій підключено (File -> Settings -> Build, execution and deployment -> Compiler -> Annotation Processing -> Enable annotation processing (прапор має бути встановлений)). Не забудьте відзначити і це: File -> Settings -> Build, execution and deployment -> Gradle -> Runner -> Delegate IDE build/run actions to cradle.
Додавання анотації @Inject
План - впровадити залежності Starks і Boltons у клас War за допомогою Dagger 2. Про що ми повинні явно йому сказати. Нижче приклад того, як це потрібно робити, використовуючи впровадження з використанням конструктора.
public class Boltons implements House {
@Inject //Dagger 2
public Boltons() {
}
@Override
public void prepareForWar() {
// щось відбувається
System.out.println(this.getClass().getSimpleName() + " prepared for war");
}
@Override
public void reportForWar() {
// щось відбувається
System.out.println(this.getClass().getSimpleName() + " reporting..");
}
}
public class Starks implements House {
@Inject //Dagger 2
public Starks() {
}
@Override
public void prepareForWar() {
// щось відбувається
System.out.println(this.getClass().getSimpleName() + " prepared for war");
}
@Override
public void reportForWar() {
// щось відбувається
System.out.println(this.getClass().getSimpleName() + " reporting..");
}
}
Ці дві залежності використовуються в конструкторі класу War, де ми повинні це відзначити.
План полягає в тому, щоб зробити залежність або об'єкт класу War
доступним усім іншим класам. Але для роботи класу War необхідно надати йому два класи, від яких він залежить - Starks і Boltons.
public class War {
private Starks
private Boltons boltons;
@Inject
public War(Starks starks, Boltons bolton) {
this.starks = starks;
this.boltons = bolton;
}
public void prepare() {
starks.prepareForWar();
boltons.prepareForWar();
}
public void report() {
starks.reportForWar();
boltons.reportForWar();
}
}
Маркування конструктора анотацією @Inject: це сигнал для Dagger, що він може створювати екземпляри цього класу. Коли Dagger зустрічає @Inject у конструкторі, він автоматично бере на себе створення цього об'єкта.
Додавання анотації @Component
Як ми дізналися раніше, @Component - це міст між кодом, що генерується, і залежностями. Також @Component говорить Dagger 2 як необхідно впроваджувати залежність. Зробимо інтерфейс BattleComponent всередині класу BattleOfBastards (можна зробити й окремо).
@Component
interface BattleComponent {
War getWar();
}
Цей інтерфейс буде реалізовано класом, який згенерує Dagger 2, а функція getWar() поверне екземпляр War, який ми зможемо використовувати в потрібному місці.
Тепер необхідно перезібрати (rebuild) проєкт!
Після складання проєкту ви побачите, що Dagger 2 згенерував клас під назвою DaggerBattleComponent - він допоможе нам впровадити клас War. Використовуємо цей клас для отримання екземпляра War.
@Component
interface BattleComponent {
War getWar();
}
public class BattleOfBastards {
public static void main(String[] args){
// Ручне впровадження залежностей
// Starks starks = new Starks();
// Boltons boltons = new Boltons();
// War war = new War(starks,boltons);
// war.prepare();
// war.report();
// Використання Dagger 2
BattleComponent component = DaggerBattleComponent.create();
War war = component.getWar();
war.prepare();
war.report();
}
}
Використовуючи клас DaggerBattleComponent, ми можемо використовувати метод getWar(), який повертає екземпляр War, що впроваджує залежності від Starks і Boltons.
Резюме
Ми обговорили як ручне використання DI ускладнює роботу і збільшує кількість шаблонного коду. Далі обговорили як Dagger 2 допомагає нам позбутися цього болю і сам генерує шаблонний код.
Після розібрали інформацію щодо обробників анотацій і базових анотацій Dagger 2 (@Inject і @Component). Потім застосували анотації в нашому прикладі та впровадили залежності, використовуючи Dagger 2.
