Автор: Константин Марс
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 “Tasting Dagger 2 on Android”:
http://fernandocejas.com/2015/04/11/tasting-dagger-2-on-android/
- Miroslav Stanek “Dagger 2 – graf creation performance/:
http://frogerms.github.io/dagger-creation-performance/
- Dagger2 official page:
http://google.github.io/dagger/
Рекомендую почитать Fernando Cejas про паттерны проектирования. Мирослав Станек очень хорошо описал скоупы. У него есть замечательная статья о том, как правильно управлять @Retention(RUNTIME) и вовремя чистить память. И, конечно, посетите официальную страницу Dagger2.
Смысл кода
Как мы организовали быструю Agile-разработку с использованием Mock-модулей и в итоге обогнали сервер-сайд.
История такова. Dagger 2 мы использовали в проекте с юнит-тестами, c правильным разделением MVP, но ключевым моментом было, что сервер-сайда на тот момент не было. А мы писали приложение, которое должно с сервера забрать данные, показать, все это красиво обработать, проанализировав данные. Основная задача стояла, чтобы при появлении REST сервисов мы смогли быстро на них перейти. Быстро это сделать, можно только меняя код вручную. При наличии модулей и компонентов, после появления сервера, мы легко заменили Mock-данные (которые поставлялись с помощью инжекции) на реальные данные с REST API, заменив только один модуль и одну строку кода в объявлении корневого компонента.
Комментарии (15)
drc
06.05.2016 15:24Пожалуйста, не растягивайте картинки с текстом, а еще лучше примеры кода тоже текстом выкладывать.
brooth
06.05.2016 15:24Около года ждал Dagger2. Когда вышел релиз, с радостью начал выпиливать Roboguice и переводить проекты на даггер. Чем дальше шло дело, тем сильнее я чесал затылок. Для лучшей библиотеки по инжекции все настраивается крайне топорно. Писать inject(Xyz) для каждого(!) класса. Из коробки нельзя подменить провайдера (в тестах например), только через костыли. Обращение к сгенерированному коду. Все уже не вспомню.
Поэтому я решил написать свой DI фреймфорк (http://jeta.brooth.org/guide/inject.html) с покером и без рефлекции. И таки написал. Все работает, расширяется и удобно. Теперь чешу затылок, или я чего не понимаю или они.Artem_zin
06.05.2016 20:22Рефлекшн в вашем DI: тыц.
brooth
06.05.2016 21:00Вообще, когда мы говорим о рефлекшене в DI, я думаю мы подразумеваем нахождение элементов с аннотаций
@Inject
используя Java Reflection API, неClass.forName()
. Потом,ClassForNameMetasitory
не основной класс библиотеки, скорее на всякий случай, в документации его нет. Тем не менее, основной класс-metasitory —MapMetasitory
, но он тоже использует cClass.forName()
. Но, если вопрос принципиальный, то можно и это обойти просто создав сгенерированныйMapMetasitoryContainer
как в статье создаетсяDaggerAppComponent
.
Хотя, при всей моей не любви в рефлекции, я не вижу ничего плохого в вызовеClass.forName()
, тем более один раз при старте.Artem_zin
06.05.2016 21:11Class.forName() печален тем, что надо исключать классы из обфускации, в остальном мне тоже не особо принципиально.
Про Dagger 2: мне нравится то, как он ложится в концепцию языка и делает DI не меташтукой, а полноценными и понятными сущностями, которые легко подменять при тестировании и интегрировать в разные места приложения.
brooth
06.05.2016 21:57Я не согласен с тем что зависимости легко подменять, по крайней мере в версии 1.0. Мне приходилось оборачивать все провайдеры в отдельный класс и передовать его (или подменять тестовым) в модуль:
@Module public class RestModule { protected final RestModuleProviders providers; public RestModule(RestModuleProviders providers) { this.providers = providers; } @Provides RestManager provideRestManager() { return providers.provideRestManager(); } }
Как это делается в Jeta:
@MetaEntity(ext = RestManager.class) class TestRestManager extends RestManager { //... }
И вы меня простите конечно, но о каком удобвстве можно говорить, когда вам предлагают писатьinject(MyThousandthClass c)
для каждого класса где вы используете инжекцию?
В остальном в Jeta DI все соблюдено, никакой мета-магии, все классы сгенерированны, читаемы и легко дебажатся. По возможности все ошибки проверяются в compile-time, и все зависимости, провайдеры и скопы определяются там же.Artem_zin
06.05.2016 22:25Так в том то и дело, что я могу отнаследовать или замокать модуль и переопределить метод
@Provides
, это просто класс, который провайдит что-то, и он никуда от меня не спрятан.
И вы меня простите конечно, но о каком удобвстве можно говорить, когда вам предлагают писать inject(MyThousandthClass c) для каждого класса где вы используете инжекцию?
1) Так сразу видно что куда инжектится, понятен скоуп зависимостей и вот это всё.
2) Интерфейс@Component
легко подменяется в тестах на тестовую реализацию/мок.
Ваш пример:
@MetaEntity(ext = RestManager.class) class TestRestManager extends RestManager { //... }
Не понятно для какой области проекта будет использован
TestRestManager
, что если уTestRestManager
есть параметры, про которые DI не знает? С даггером можно написать этот код руками (а можно и не писать) и сделать всё что нужно.
С Dagger1 я воевал, читал исходники, искал пути внедрения нужного мне поведения (в основном для тестов) и тд и тп. В исходники Dagger 2 не залазил ни разу тк всё что нужно можно делать из "юзерспейса" и даггер просто будет использовать тот код, который я ему подсуну, для меня это библиотека для DI, наиболее натурально встраивающаяся в язык .
brooth
06.05.2016 22:52замокать модуль
Если у Вас только тестовый модуль то вы конечно можете замокать. У меня проект где провайдеры могут быть заменены не только в тестах но и в расширениях (вся логика приложения находится в библиотеки (aar), а уже на ее основе создаются приложения(apk))
Так сразу видно что куда инжектится, понятен скоуп зависимостей и вот это всё.
что если вам нужно проинжектить класс с разными скопами?
Не понятно для какой области проекта будет использован
Тут все описано. Скоп можно указать как аргумент аннотации@MetaEntity(ext = RestManager.class, scope=AppScope.class)
, а можно указать дефолтный в настройках.
что если у TestRestManager есть параметры, про которые DI не знает?
Простите, о каких параметрах речь?
для меня это библиотека для DI, наиболее натурально встраивающаяся в язык
На самом деле рад что Вас все устраивает. Как в анекдоте, «Если все работает — #%& трогать»Artem_zin
06.05.2016 22:57Если у Вас только тестовый модуль то вы конечно можете замокать. У меня проект где провайдеры могут быть заменены не только в тестах но и в расширениях (вся логика приложения находится в библиотеки (aar), а уже на ее основе создаются приложения(apk))
Тут Dagger 2 вроде палок в колёса не вставит, можно акцептить модули снаружи — из конкретных приложений.
Простите, о каких параметрах речь?
Если для работы
TestRestManager
нужны будут объекты, о которых DI не знает, как он создаст инстансTestRestManager
?
// P.S. я ни в коем случае не наезжаю и не осуждаю ваш DI, со стороны вроде ок, эдакий микс Dagger 1 и Dagger 2, разве что слишком часто встречается
Meta
в названиях классов, что смущает и сразу думаешь про XML :D
awsi
06.05.2016 23:31что если вам нужно проинжектить класс с разными скопами?
Это как?
Скопы (по крайней мере в Android), могут быть только вложенными, т.е. вы не можете проинжектить класс с двумя скопами. Вы можете вынести его в вышестоящий скоп.
awsi
06.05.2016 22:42А почему не использовали dagger от Square? В нем можно «оверайдить» модули, т.е. легко подменять зависимости (кроме, как в тестах этого и не нужно). Правда, с использованием того же рефлекшен, который для Вас не является критическим аргументом.
brooth
06.05.2016 23:10Честно, я особо не знаком с dagger1. Отказался от него на фазе research. Как минимум, тут тоже нужно перечислять все классы которые я собираюсь инжектить. И для меня критично использование рефлекшена, обратного я не говорил. Ну и кончено, мысль от том что гугл собрался переписать этот фреймфорк намекала что с ним не все хорошо)
Zeliret
Очень крутая и полезная либа.