Dagger 2. Лікуємо залежності за методикою Google


Автор: Костянтин Марс
Senior Developer @ DataArt,
Co-Organizer @ GDG Dnipro

Dependency Injection

Що, навіщо і коли це потрібно
Сьогодні ми поговоримо про інструмент, який допомагає поліпшити якість розробки для Android. Вирішити це завдання можна за допомогою Dependency Injection (DI). Зазвичай цей термін асоціюється з ін'єкціями, шприцами і трошки з «залежностями». Насправді, Dependency Injection — шаблон проектування, що забезпечує реалізацію принципу інверсії залежностей і реалізує правила створення об'єктів і незалежність реалізацій.
Отже, у нас є клас, у класу є конструктор, і є кілька членів класу. Коли ви створюєте сутність цього класу, вам необхідно забезпечити клас инстансами тих самих типів, які оголошені для його членів класу. В даному випадку, це ім'я машини і тип двигуна Engine. Ви будете використовувати посилання на об'єкти, відповідно, посилання всередині вашого класу не будуть пустувати.
Таким чином, ви реалізуєте ООП і можете створювати об'єкти.

Створення класів породжує...



  • Композиція — не успадкування.
  • Посилання не будуть пустувати. 


Можливість створювати об'єкти...
Ви можете створити об'єкт, задати ім'я машини і створити якийсь новий двигун.



Доступно створення різних об'єктів, наприклад, створення двигуна іншого типу або просто іншого двигуна.



Наприклад, ви можете створити два різних об'єкта, які будете використовувати. В даному випадку, той самий двигун від «Патріота». Відповідно, якщо ви поставите цей двигун у Jeep Grand Cheerokee — це буде трохи дивно. Але, тим не менш, ви можете це зробити. При цьому використовується так званий патерн «композиція », коли сутності, які ви створюєте, будуть включатися в іншу сутність, і це буде, як ви бачите, не наслідування, а саме композиція.



Тут все дуже просто: якщо ви подивитеся на SuperTunedEngine, зрозумієте, що насправді він є спадкоємцем певного типу, який вже оголошено заздалегідь, а також, можливо, є реалізацією інтерфейсу — для нас це не принципово. В даному випадку Engine може бути інтерфейсом.



Дивлячись на два оголошення, ви бачите: ми можемо зробити так, що два об'єкти будуть залежати від будь-якого іншого об'єкта. Власне, це і є залежність. Таким чином, можливість створювати об'єкти породжує залежно — досить банальна річ.

… залежно 
Car depends on Engine. Engines may vary. We'll probably need different engines for testing and production.



Як ви бачите на схемі (зображення не з нашого прикладу), залежно бувають дуже різні. У вас будуть залежні сервіси, залежні activity, презентери, в'ю, контролери. Всі ці сутності переплетені між собою залежностями. Якщо спробувати виразити це графічно, вийде приблизно те, що ви зараз бачите на картинці.
В реальній робочій системі залежностей буде набагато більше. Тести, які проводять відомі компанії, що надають інструментарій для тестування Android-додатків, показують, що залежностей, навіть у простих, на перший погляд, додатках, бувають тисячі. У середньому тисячі і десятки тисяч залежностей зустрічаються навіть у найпростіших додатках. Щоб реалізовувати ці залежності, як можна більш ефективно, не инстанцируя кожен раз усередині свого класу якісь інші класи і не дописуючи купу коду, який буде повторюватися і додавати вам зайву роботу, існує інструмент Dagger.

Dagger and JSR-330 Standart

Анотація Inject
Dagger заснований на стандарті JSR-330. Цей стандарт Google використовує дуже давно, і це — стандарт для Java Injection.

Трохи ще НЕ історії 
  • Dagger 2 — Google, Greg Kick
  • Dagger — Square, Jake Wharthon
  • Guice — Google, Jesse Wilson 
Заглянемо трохи в історію: Google колись створив такий продукт як Guice (в народі його називають «Джус», а в наших широтах — «Гуска»). Guice працював з рефлексією, він слідував анотацій, але згодом розробники з Square вдосконалили систему, яка була в Guice і створили Dagger1. 
Dagger1 був крутим інструментом, але, як показує практика, тут можна щось покращити. До речі, Dagger1 теж використовував рефлексію. І в 2015 р. розробники з Google випустили Dagger2. Здається, що ще зовсім недавно Jake Wharton (відомий розробник з компанії Square) анонсував його випуск з прицілом на осінь — обіцянку виконано, у нас є якісний і випереджає конкурентів за результатами тестів продукт.

Інверсія управління (англ. Inversion of Control, IoC)



Повернемося до стандартів і термінології. Отже, у нас є продукт, який з'явився в ході еволюції. Він використовує JSR-330, який, надає цілий ряд анотацій. Крім того, він слідує певним принципам, свого роду паттернам розробки, один з яких — Inversion of control (IoC). 
Процес надання зовнішньої залежності програмного компоненту є специфічною формою «інверсії контролю» (англ. Inversion of control, IoC), коли вона застосовується до управління залежностями. У відповідності з принципом single responsibility об'єкт віддає турботу про побудову необхідних йому залежностей зовнішнього, спеціально призначеному для цього загального механізму.
Ця річ пов'язана з архітектурними патернами. Ми повинні писати додаток таким чином, щоб внутрішні класи, пов'язані з доменної логікою, не залежали від зовнішніх класів, щоб додаток було написано грунтуючись на інтерфейсах. Таким чином реалізується розмежування зони відповідальності. Звертаючись до якоїсь реалізації, ми звертаємося в першу чергу до інтерфейсу. Inversion of Control реалізується через Dependency Injection власне сам інструментарій називається Dependency Injection (DI). 

Reflection vs Compile time
  • Dagger2 vs Dagger1


Dagger2 використовує кодогенерацию, на відміну від Dagger1, який використовував рефлексію.

JSR-330

JSR-330 a.k.a. javax.inject
  • Inject, Qualifier, Scope. etc.
  • Standardized Dependency Injection API
  • Reference Implementation: Google Guice 2.0
  • Also supported by Spring since 3.0
  • Defines API, not injector implementation or configuration
JSR описує не тільки анотацію Inject, але і надає цілий пакет анотацій, які дозволять вам декларувати яким чином будуть взаємодіяти сутності для забезпечення Dependency Injection.
Наприклад, я розповідаю про певний сімействі Dependency Injection-продуктів, які слідують цим стандартам. Є інші продукти, які цього стандарту не наслідують, про них ми сьогодні говорити не будемо, але вони існують. Inject, Qualifier, Scope   про них ми поговоримо пізніше. Ці анотації не були створені тільки для Dagger2, вони існують і для інших інжекторів, наприклад, Guice.

Отже, настав час додати в наш код трохи магії...



Ми почнемо з того, що аннотируем члени класу анотацією inject. Все досить просто. Щоб инстанцировать в подальшому ці залежності і наш інструментарій Dependency Injection зміг правильно підібрати куди саме инстанцировать і що, ми повинні також анотувати конструктор. Тут ситуація стає трохи цікавіше.

Зверніть увагу на конструктор за замовчуванням



Найпростіший спосіб забезпечити інжекції — створити конструктор за замовчуванням. Потім анотувати инжектом сам конструктор за замовчуванням і ті члени, які вимагають инстанцирования цього класу. Це робиться дуже просто.

Конструктор з параметрами — гарне місце для модифікацій



В реальному житті нам знадобляться конструктори з параметрами. Деякі з них система зможе підібрати автоматично, якщо у них є конструктори за замовчуванням. А деякі, наприклад, той же Engine, можливо доведеться конструювати вручну. 
Також ви будете інжектувати презентери з допомогою таких конструкторів, це дуже часто використовується в MVP (Model-View-Presenter).

І все ж таки — як це змусити працювати?



Структура інжекції Dagger2.0
Структура інжекції — взаємозв'язок компонентів Dagger, які дозволяють нам об'єднати анотації inject і об'єднати оголошення класів. 

Компоненти та Модулі


Pic. author — Miroslaw Stanek from Azimo
http://frogermcs.github.io/dagger-graph-creation-performance/
Структура інжекції Dagger включає в себе компоненти та модулі. Якщо ви подивіться на картинку (статті Мирослава Станек з компанії Azima), побачите, що компоненти — контейнери для модулів, причому всередині одного компонента можуть бути інші. Пізніше ми побачимо, що компоненти, які вкладені, називаються підкомпоненти (@SubСomponent). І називати їх просто «компоненти» ми не можемо за правилами інжекції.

Модуль — колекція генераторів



Анотація Module   анотація, яка говорить, що ось ця сутність — ось цей клас — є модулем, який буде генерувати инстансы об'єктів.
Тут теж все досить просто. Анотації, які генерують, — це анотація provides. Анотація provides просто вказує, що даний метод модуля буде постачати вам сутність. Найцікавіше відбуватиметься всередині цього методу. 
Вам необхідно буде, слідуючи цим правилам, якимось чином инстанцировать об'єкт. Деякі об'єкти будуть залежати один від одного. Деякі об'єкти будуть залежати від членів класу модуль, які ви можете зберігати в модулі. Наприклад, той же контекст, ви можете покласти в модуль. Модуль про нього пам'ятає і потім, при инстанцировании тих же самих прензентеров, ви будете генерувати нові сутності презентеров на основі контексту, який модуль запам'ятав один раз при його створенні.
Як ви бачите, у модуля є конструктор. В даному випадку замість контексту ми передаємо Application. При створенні чогось нового можемо повертати те, що зберігається в самому модулі.

Що таке сінглетон (англ. Singleton)? 
При створенні деяких сутностей, ми ставимо певні параметри. Сінглтон (англ. Singleton) — інструкція, яка говорить, що там, де інжектор буде знаходити анотацію inject, він не повинен инстанцировать новий об'єкт, а має переиспользовать вже истанцированный вже один раз об'єкт-сінглетон. 

@Component
Компонент — хост для модулів, інжектор для класів, корінь дерева залежностей.



З компонентом все трохи цікавіше. Компонент повинен враховувати час життя модулів, які він включає. Якщо ми спробуємо використовувати сінглтон для компонента, який використовує час життя инстанцирования, виникнуть конфлікти. Тому потрібно чітко розуміти, що, наприклад, компонент для Application, буде синглетоном, тому що об'єкт класу Application існує в єдиному екземплярі, і існують весь час життя додатки. Для activity, наприклад, це теж може бути сінглетон, і його час життя буде прив'язане до часу життя activity. При необхідності існує можливість анотувати компонент додаткової анотацією Singleton. Є список модулів, який включає в себе компонент.
Наприклад, у Activity буде анотація inject. У компоненті повинні бути вказані модулі, які роблять provides цієї activity. Ви повинні обов'язково вказати в компоненті, куди ми инжектируем. Тобто ми повинні вказати конкретний клас, причому, зверніть увагу, що тут не можна, наприклад, написати BaseActivity як базовий клас, тому що тоді інжекція відбудеться тільки в Base aActivity, а в MainActivity, куди потрібно, наприклад, проинжектить якийсь презентер, правила будуть трохи іншими.



Метод inject — опис того, хто залежить. Модулі — опис тих, хто надає залежності.
Давайте повернемося до модулю. Модуль оголошується як клас. Важливо зауважити, що модуль — реальний клас, який має справжні посилання на реальні об'єкти. І він створюється вами вручну при оголошенні компонента, при зв'язці. Компонент, у свою чергу, — об'єкт, який генерує Dagger. Як раз в цей момент відбувається магія кодогенерации. Тому компонент оголошується як інтерфейс. 

Ініціалізація компонента generated code used



Ось, наприклад, DaggerAppComponent ініціалізується всередині нашої програми. Зверніть увагу, generated code used (ініціалізація компонента) означає, що ми використовуємо генерований код. Наприклад, можна виявити DaggerAppComponent. Як ви бачили раніше, ніякого префікса Dagger не було. Звідки ж він з'явився? Так, Dagger згенерував код. Він генерує досить швидко. Якщо ви випадково поламаєте інжекції, про яку ми зараз говоримо (про її структуру), в результаті у вас DaggerAppComponent не з'явиться. Якщо ви допустили невеличку помилку і неправильно вказали клас, генерація не спрацює — DaggerAppComponent не з'явиться, і вся магія, яка забезпечує нам прив'язку наших activity і інших класів, не запрацює без згенерованого класу. Тому що компонент є коренем усього дерева — це основа. І без нього все інше не працює. Слід уважно ставитися до того, як ми будуємо інжекції до цього, і правильно використовувати компонент.
Також слід зазначити, що у компонента є builder. Builder — шаблон проектування. Ми розуміємо, що у білдера є якісь аргументи, які визначають, як буде будуватися далі наш компонент, наприклад, метод AppModule — автоматично згенерований метод, який приймає в якості аргументу інстанси-класом AppModule. Модуль ми створюємо руками і задаємо параметри. І викликаємо метод build для отримання AppComponent. У цьому посиланню є приклад з реального коду: http://github.com/c-mars/Dagger2Scopes.git.

Inject This! :)



Puttin' magic will work only after injection… :)
У класу Application є методи, які надають доступ до нього. Тим більше, це не статичний метод, ви можете просто отримати з get application контекст. Можете його прикастить до свого класу — вийде те ж саме і ніякої магії тут не буде. Але, що дійсно важливо, у нас буде цей getAppComponent.
Ідея в тому, що Application зберігає AppComponent. Ми викликаємо якісь додаткові методи на цьому компоненті, і потім застосовуємо метод inject. Як ви помітили, це той інжект із зазначенням конкретного класу, який ми оголосили в компоненті. В даному випадку це — клас LoginActivity. Ви бачите в анотації інжект, бачите, як ми заинжектили залежності. Магія запрацює тільки після инжекшена.

Custom Scopes і ефективне управління пам'яттю
Custom Scopes, як такі, що служать для того, щоб забезпечити вашим истанцированным класів певний час життя.

Життєвий цикл об'єктів


Pic. author — Miroslaw Stanek from Azimo
http://frogermcs.github.io/dagger-graph-creation-performance 
Наприклад, у вас є activity, і вони живуть досить недовго, з одного екрана на інший переходите і все вбиваєте. Тобто все, що заинжекшено у activity, цілком можна після цього почистити, і додаток буде споживати менше пам'яті. Якийсь клас користувальницьких даних, наприклад, User, буде жити між логінами. Application Scope — найголовніший, кореневої scope, живе довше за всіх.

І ще раз та ж матрьошка
Компонент має область життя (scope)


Pic. author — Miroslaw Stanek from Azimo
http://frogermcs.github.io/dagger-graph-creation-performance/

This mysterious 'plus'...



Тепер звернемо увагу на плюс.

Оголошення субкомпоненту



Анотація Scope дозволяє генерувати скоупы певного часу життя. Наприклад, ActivityScope буде жити стільки, скільки живе activity. Їм аннотируются компоненти як підкомпоненти.

Але ж там був модуль!



Дані всередині описуються також, як в кореневому вікні. За винятком однієї речі: коли ви викличете плюс, передасте туди модуль і инстанцируете його, ви отримаєте подкомпонент, який вам потрібен.

Додавання субкомпоненту до кореня дерева залежностей



Для чого воно використовується? Щоб скоупы, які можна оголошувати як інтерфейси, обмежували час життя наших об'єктів. 

Анотація Scope





Цей вид Scope буде обмежувати час життя статично, в залежності від того, куди ви заинжектились. А інший буде обмежувати динамічно.
Динамічний означає, що ви будете керувати ним вручну, і все буде віддалятися з допомогою garbage collector («збирач сміття»). 
Ми аннотируем компонент, необхідний скоупам.

@ActivityScope





@UserScope, наприклад, як він буде працювати цей той самий скоуп, який має @Retention(RUNTIME). Ним можна буде управляти вручну.  

@UserScope



Щоб їм управляти вручну, ви зберігаєте посилання на компонент усередині додатку поруч з AppComponent.



Він створюється з допомогою особливого методу, приклад коду, який ви можете побачити. Потім код почистили, відправили його в реліз, і garbage collector його видалить. Коли це відбувається? Коли користувач вийшов з системи («вылогинился»). Наступного разу, коли ви викличете ще один createUserComponent, цей компонент створиться ще раз з іншими даними юзера.

Наостанок… Що инжектить?
  • Модулі демо-даних.
  • Презентери.
  • Синглтоны.
  • Тестові реалізації класів.
  • … Все інше, що инстанцируется і створює залежності.
Насправді, инжектить треба те, що допоможе ефективніше инжектить пам'ять і писати код. Презентери однозначно повинні використовуватися. 
Синглетоны — це зручно. У прикладі, який я зараз наведу, ми инжектили Mock-дані для демо-версії, і їх можна було використовувати з варіаціями при тестуванні. 
Home readings
Sample code: http://github.com/c-mars/Dagger2Scopes.git
Рекомендую почитати Fernando Cejas про патерни проектування. Мирослав Станек дуже добре описав скоупы. У нього є чудова стаття про те, як правильно управляти @Retention(RUNTIME) і вчасно чистити пам'ять. І, звичайно, відвідайте офіційну сторінку Dagger2.
Сенс коду
Як ми організували швидку Agile-розробку з використанням Mock-модулів і в підсумку обігнали сервер-сайд.
Історія така. Dagger 2 ми використовували в проекті з юніт-тестами, c правильним поділом MVP, але ключовим моментом було те, що сервер-сайда на той момент не було. А ми писали додаток, яке повинно з сервера забрати дані, показати, все це красиво обробити, проаналізувавши дані. Основне завдання стояло, щоб при появі REST сервісів ми змогли швидко на них перейти. Швидко це зробити, можна тільки змінюючи код вручну. При наявності модулів і компонентів, після появи сервера, ми легко замінили Mock-дані (які поставлялися з допомогою інжекції) на реальні дані з REST API, замінивши тільки один модуль і один рядок коду в оголошенні кореневого компонента.

Джерело: Хабрахабр

0 коментарів

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