Dagger 2. Лечим зависимости по методике Google
Автор: Константин Марс
Senior Developer @ DataArt,
Co-Organizer @ GDG Dnipro
Dependency Injection
Что, зачем и когда это нужно
Сегодня мы поговорим об инструменте, который помогает улучшить качество разработки для Android. Решить эту задачу можно с помощью Dependency Injection (DI). Обычно этот термин ассоциируется с инъекциями, шприцами и немножко с «зависимостями». На самом деле, Dependency Injection — паттерн проектирования, обеспечивающий реализацию принципа инверсии зависимостей и реализующий правила создания объектов и независимость реализаций.
Итак, у нас есть класс, у класса есть конструктор, и есть несколько членов класса. Когда вы создаете сущность этого класса, вам необходимо обеспечить класс инстансами тех самых типов, которые объявлены для его членов класса. В данном случае, это имя машины и тип двигателя Engine. Вы будете использовать ссылки на объекты, соответственно, ссылки внутри вашего класса не будут пустовать.
Таким образом, вы реализуете ОOП и можете создавать объекты.
Создание классов порождает…
- Композиция — не наследование.
- Ссылки не будут пустовать.
Возможность создавать объекты…
Вы можете создать объект, задать имя машины и создать какой-нибудь новый двигатель.
Доступно создание разных объектов, например, создание двигателя другого типа или просто другого двигателя.
Предположим, вы можете создать два разных объекта, которые будете использовать. В данном случае, тот самый двигатель от «Патриота». Соответственно, если вы поставите этот двигатель в 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, заменив только один модуль и одну строку кода в объявлении корневого компонента.