Dagger 2. Продовження
Давайте згадаємо розглянутий раніше приклад - битву бастардів. У класу War є два сильні зв'язки: Boltons і Starks.
public class War {
private Starks starks;
private Boltons boltons;
public War(){
starks = new Starks();
boltons = new Boltons();
starks.prepareForWar();
starks.reportForWar();
boltons.prepareForWar();
boltons.reportForWar();
}
}
Настав час скористатися драконячим склом і знищити Білих Ходоків. Так, ми будемо використовувати впровадження залежностей, щоб усунути сильні зв'язки.
Згадаймо ідею впровадження залежностей. Клас не повинен створювати інші класи. Натомість він має отримувати залежності ззовні. Давайте отримаємо залежності Boltons і Starks зовні, через конструктор класу War.
public class War {
private Starks starks;
private Boltons boltons;
// Впровадження залежностей(DI) - отримання залежностей з іншого місця через конструктор
public War(Starks starks, Boltons boltons){
this.starks = starks;
this.boltons = boltons;
}
public void prepare(){
starks.prepareForWar();
boltons.prepareForWar();
}
public void report(){
starks.reportForWar();
boltons.reportForWar();
}
}
Приклад вище - це варіант впровадження залежностей через конструктор. Ви, напевно, зараз думаєте, що отже постійно робите це у своїх проєктах. Правильно, багато хто використовує концепцію впровадження залежностей, не підозрюючи про це.
Клас War повинен знати не тільки про те, як виконати певне завдання, а й те, де шукати класи, яких він потребує для виконання своїх завдань. Якщо ми надамо все необхідне для роботи нашому класу зовні, то позбудемося раніше розглянутих проблем. Клас легко зможе працювати з будь-якими екземплярами інших класів, які потрібні йому для виконання завдань, і буде просто тестуватися в ізоляції від них. У застосунку, що використовує впровадження залежностей, об'єкти ніколи не шукатимуть залежності або створюватимуть їх усередині себе. Усі залежності надаються йому або впроваджуються в нього готовими до використання.
У якийсь момент, звісно, хтось має створити екземпляри класів залежностей і надати їх об'єктам, які цього потребують. Зазвичай така робота виконується в точці входу в додаток. У звичайному Java-додатку, наприклад, такий код можна знайти всередині методу main(), як показано нижче. В Android це зазвичай робиться в методі onCreate() всередині Activity.
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();
}
}
У класі BattleOfBastards ми створюємо залежності Boltons і Starks і впроваджуємо їх через конструктор у клас War. Залежний клас War залежить від залежностей Boltons і Starks.
Час гідно оцінити себе і відсвяткувати. Так, ми знищили Білих Ходоків (сильні зв'язки)! Сподіваюся, ви зрозуміли концепцію того, що ми намагаємося розібрати.
Резюме
У цій статті ми зрозуміли, що клас не повинен створювати залежності всередині себе. Натомість, він має отримувати їх ззовні.
Також ми побачили план простого впровадження залежностей у дії. Ми взяли приклад з битвою бастардів і спробували усунути сильні зв'язки за допомогою впровадження залежностей.
media
Анатомія DaggerBattleComponent
Для кращого розуміння Dagger 2 розглянемо клас DaggerBattleComponent. Встановіть курсор на DaggerBattleComponent і натисніть Ctrl+B (або Ctrl+ЛКМ, Command+ЛКМ). Ви побачите таке:
@Generated(
value = "dagger.internal.codegen.ComponentProcessor",
comments = "https://google.github.io/dagger"
)
public final class DaggerBattleComponent implements BattleComponent {
private DaggerBattleComponent(Builder builder) {}
public static Builder builder() {
return new Builder();
}
public static BattleComponent create() {
return new Builder().build();
}
// реалізований метод інтерфейсу
@Override
public War getWar() {
return new War(new Starks(), new Boltons());
}
public static final class Builder {
private Builder() {}
public BattleComponent build() {
return new DaggerBattleComponent(this);
}
}
}
Ось що генерує Dagger 2 за нас для вирішення проблеми сильних зв'язків (hard dependency). Якщо подивитися на інтерфейс, який реалізує клас, то ви побачите, що це BattleComponent - інтерфейс, який ми раніше створили й описали в ньому метод getWar() для надання екземпляра класу War.
Ця залежність надається з використанням шаблону builder.
Вивчимо дещо нове
Я сподіваюся, що ви чітко розумієте, навіщо потрібен метод getWar(). Зараз я хочу додати ще пару залежностей: Starks і Boltons. Додамо методи в інтерфейс:
@Component
interface BattleComponent {
War getWar();
// додаємо методи
Starks getStarks();
Boltons getBoltons();
}
Після внесення змін заново зберіть проєкт. Тепер перевіримо клас DaggerBattleComponent. Якщо ви все зробили правильно, то побачите таке.
@Generated(
value = "dagger.internal.codegen.ComponentProcessor",
comments = "https://google.github.io/dagger"
)
public final class DaggerBattleComponent implements BattleComponent {
private DaggerBattleComponent(Builder builder) {}
public static Builder builder() {
return new Builder();
}
public static BattleComponent create() {
return new Builder().build();
}
@Override
public War getWar() {
return new War(getStarks(), getBoltons());
}
@Override
public Starks getStarks() {
return new Starks();
}
@Override
public Boltons getBoltons() {
return new Boltons();
}
public static final class Builder {
private Builder() {}
public BattleComponent build() {
return new DaggerBattleComponent(this);
}
}
}
Як видно, Dagger 2 реалізував методи getStarks() і getBoltons().
Ми вказали Dagger 2 отримати ці залежності за допомогою анотації @Inject у класі Boltons. Давайте дещо зламаємо. Приберіть анотацію @Inject з класу Boltons. Зберіть проєкт заново.
Нічого не сталося? Так, ви не отримали жодної помилки, але спробуйте запустити проєкт. Ви повинні отримати наступну помилку:

Якщо прочитаєте текст помилки, то він явно говорить про те, що методи getWar() і getBoltons() не працюватимуть, якщо немає позначок анотаціями @Inject або @Provides.
Як раніше згадувалося, Dagger 2 дає змогу легко відстежувати помилки. Можете трохи погратися з цим класом.
Анотації @Module і @Provides
Копнемо глибше і розберемося з парою корисних анотацій - @Module і @Provides. Їх варто використовувати, якщо розмір вашого проєкту збільшується.
@Module
Якщо коротко, то ця анотація позначає модулі та класи. Поговоримо про Android. У нас може бути модуль ContextModule і цей модуль буде надавати залежності ApplicationContext і Context для інших класів. Для цього ми повинні позначити клас ContextModule анотацією @Module.
@Provides
Якщо коротко, то ця анотація потрібна для позначки методів, які надають залежності, всередині модулів. У раніше описаному прикладі ми позначили клас ContextModule анотацією @Module, але також нам необхідно позначити методи, які надають залежності ApplicationContext і Context анотацією @Provides.
Подивіться невеликий приклад (посилання на гілку).
Приклад
Візьмемо два сервіси, що надаються Браавосом, - гроші (Cash) і солдатів (Soldiers) (Я не впевнений, що вони надають такі послуги, але розглянемо це тільки для прикладу). Створимо два класи:
public class Cash {
public Cash(){
// щось відбувається
}
}
public class Soldiers {
public Soldiers(){
// щось відбувається
}
}
Тепер створимо модуль і назвемо його BraavosModule. Він постачатиме нам дві залежності - Cash і Soldiers.
@Module // Модуль
public class BraavosModule {
Cash cash;
Soldiers soldiers;
public BraavosModule(Cash cash, Soldiers soldiers){
this.cash=cash;
this.soldiers=soldiers;
}
@Provides // Надає залежність Cash
Cash provideCash(){
return cash;
}
@Provides // Надає залежність Soldiers
Soldiers provideSoldiers(){
return soldiers;
}
}
Як ми бачили раніше, необхідно позначити всі модулі анотацією @Module, а методи, що надають залежності - анотацією @Provides.
Повернемося до класу BattleOfBastards і вкажемо компоненту реалізовувати методи provideCash() і provideSoldiers().
@Component(modules = BraavosModule.class)
interface BattleComponent {
War getWar();
Cash getCash();
Soldiers getSoldiers();
}
public class BattleOfBastards {
public static void main(String[] args){
Cash cash = new Cash();
Soldiers soldiers = new Soldiers();
BattleComponent component = DaggerBattleComponent
.builder()
.braavosModule(new BraavosModule(cash, soldiers))
.build();
War war = component.getWar();
war.prepare();
war.report();
// використовуємо гроші та солдатів
component.getCash();
component.getSoldiers();
}
}
Зверніть увагу на те, що модуль додано в оголошення анотації @Component. Це говорить про те, що компонент міститиме всередині себе цей модуль.
@Component(modules = BraavosModule.class)
Після всіх змін зберіть проєкт заново. Ви побачите помилку в методі .create() класу DaggerBattleComponent. Вона виникла у зв'язку з тим, що під час додавання модуля необхідно передати цю залежність Dagger 2. Виглядає це так:
BattleComponent component = DaggerBattleComponent.builder().braavosModule(new BraavosModule(cash, soldiers)).build();
Після ввімкнення всіх модулів ви можете почати використовувати їхні методи через Component.
component.getCash(); component.getSoldiers();
Якщо ви хочете переконатися, то наведіть курсор на DaggerBattleComponent і натисніть Ctrl+B (або Ctrl+ЛКМ, Command+ЛКМ). Ви побачите, що модуль BraavosModule включено в клас для надання залежностей Cash і Soldiers.
Резюме
Ми проаналізували генеровані Dagger 2 класи і помітили, що Dagger 2 використовує шаблон builder для надання залежностей. Також розглянули простий приклад використання анотацій @Module і @Provides.
