Знакомимся с Dependency Injection на примере Dagger
В данной статье мы попытаемся разобраться с Dependency Injection в Android (и не только) на примере набирающей популярность open source библиотеки Dagger
И так, что же такое Dependency Injection? Согласно википедии, это design pattern, позволяющий динамически описывать зависимости в коде, разделяя бизнес-логику на более мелкие блоки. Это удобно в первую очередь тем, что впоследствии можно эти самые блоки подменять тестовыми, тем самым ограничивая зону тестирования.
Несмотря на замудреное определение, принцип довольно прост и тривиален. Я уверен, что большинство из программистов так или иначе реализовывали этот pattern, даже порой об этом не догадываясь.
Рассмотрим упрощенную (до псевдокода) версию Twitter клиента.
В теории, диаграмма зависимостей выглядит примерно так:
Давайте взглянем как это выглядит в коде:
public class Tweeter < public void tweet(String tweet) < TwitterApi api = new TwitterApi(); api.postTweet("Test User", tweet); >> public class TwitterApi < public void postTweet(String user, String tweet) < HttpClient client = new OkHttpClient(); HttpUrlConnection connection = client.open(". "); /* post tweet */ >>
Как видим, набор интерфейсов довольно прост, поэтому использовать мы это будем примерно так:
Tweeter tweeter = new Tweeter(); tweeter.tweet("Hello world!");
Пока все идет хорошо, твиты улетают — все счастливы. Теперь возникает необходимость все это протестировать. Сразу же замечаем, что неплохо было бы иметь возможность подменять Http клиент на тестовый, чтобы возвращать какой-нибудь тестовый результат и не ломиться в сеть каждый раз. В этом случае, нам надо снять с класса TwitterApi обязанность создавать Http клиент и сгрузить эту обязанность вышестоящим классам. Наш код немного преображается:
public class Tweeter < private TwitterApi api; public Tweeter(HttpClient httpClient) < this.api = new TwitterApi(httpClient); >public void tweet(String tweet) < api.postTweet("Test User", tweet); >> public class TwitterApi < private HttpClient client; public TwitterApi(HttpClient client) < this.client = client; >public void postTweet(String user, String tweet) < HttpUrlConnection connection = client.open(". "); /* post tweet */ >>
Теперь мы видим, что при необходимости простестировать наш код, мы можем легко «подставить» тестовый Http клиент, который будет возвращать тестовые результаты:
Tweeter tweeter = new Tweeter(new MockHttpClient); tweeter.tweet("Hello world!");
Казалось бы, что может быть проще? На самом деле, сейчас мы «вручную» реализовали Dependency Injection паттерн. Но есть одно «но». Представим ситуацию, что у нас есть класс Timeline, который умеет загружать последние n сообщений. Этот класс тоже использует TwitterApi:
Timeline timeline = new Timeline(new OkHttpClient(), "Test User"); timeline.loadMore(20); for(Tweet tweet: timeline.get())
Наш класс выглядит примерно так:
public class Timeline < String user; TweeterApi api; public Timeline(HttpClient httpClient, String user) < this.user = user; this.api = new TweeterApi(httpClient); >public void loadMore(int n)*. */> public List get()*. */> >
Вроде бы все ничего — мы применили тот же подход, что и с классом Tweeter — дали возможность указывать Http клиент при создании объекта, что позволяет нам протестировать этот модуль, не завися при этом от сети. Но! Вы заметили, сколько кода мы продублировали и как нам приходится «протаскивать» Http клиент прямо из «головы» приложения? Конечно, можно добавить конструкторы по умолчанию, которые будут создавать реальный Http клиент, и использовать кастомный конструктор только при тестировании, но ведь это не решает проблему, а только маскирует ее.
Давайте рассмотрим как мы можем улучшить сложившуюся ситуацию.
Dagger
Dagger — это open source Dependency Injection библиотека от разработчиков okhttp, retrofit, picasso и многих других замечательных библиотек, известных многим Android разработчикам.
- Статический анализ всех зависимостей
- Определение ошибок конфигурации на этапе компиляции (не только в runtime)
- Отсутствие reflection, что значительно ускоряет процесс конфигурации
- Довольно небольшая нагрузка на память
- инициализация графа завизимостей (ObjectGraph)
- запрос зависимостей ( @Inject )
- удовлетворение зависимостей ( @Module / @Provides )
Запрос зависимостей (request dependency)
Чтобы попросить Dagger проиницализировать одно из полей, все что нужно сделать — добавить аннотацию @Inject :
@Inject private HttpClient client;
… и убедиться, что этот класс добавлен в граф зависимостей (об этом далее)
Удовлетворение зависимостей (provide dependency)
Чтобы сказать даггеру какой инстанс клиента необходимо создать, необходимо создать «модуль» — класс аннотированный @Module :
@Module public class NetworkModule
Этот класс отвечает за «удовлетворение» части зависимостей, запрошенных приложением. В этом классе нужно создать так называемый «провайдер» — метод, который возвращает инстанс HttpClient (аннотированный @Provide ):
@Module(injects=TwitterApi.class) public class NetworkModule < @Provides @Singleton HttpClient provideHttpClient() < return new OkHttpClient(); >>
Этим мы сказали Dagger’y, чтобы он создал OkHttpClient для любого, кто попросил HttpClient посредством @Inject аннотации
Стоит упомянуть, что для того, чтобы compile-time валидация работала, необходимо указать все классы (в параметре injects), которые просят эту зависимость. В нашем случае, HttpClient необходим только TwitterApi классу.
Аннотация @Singleton указывает Dagger’у, что необходимо создать только 1 инстанс клиента и закэшировать его.
Cоздание графа
Теперь перейдем к созданию графа. Для этого я создал класс Injector , который инициализирует граф одним (или более) модулем. В контексте Android приложения, удобней всего это делать при создании приложения (наследуемся от Application и перегружаем onCreate() ). В данном примере, я создал TweeterApp клас, который содержит в себе остальные компоненты (Tweeter и Timeline)
public class Injector < public static ObjectGraph graph; public static void init(Object. modules) < graph = ObjectGraph.create(modules); >public static void inject(Object target) < graph.inject(target); >> public class TweeterApp < public static void main(String. args) < Injector.init(new NetworkModule()); Tweeter tweeter = new Tweeter(); tweeter.tweet("Hello world"); Timeline timeline = new Timeline("Test User"); timeline.loadMore(20); for(Tweet tweet: timeline.get()) < System.out.println(tweet); >> >
Теперь вернемся к запросу зависимостей:
public class TwitterApi < @Inject private HttpClient client; public TwitterApi() < //Добавляем класс в граф зависимостей Injector.inject(this); //На этом этапе "магическим" образом client проинициализирован Dagger'ом >public void postTweet(String user, String tweet) < HttpUrlConnection connection = client.open(". "); /* post tweet */ >>
Заметьте Injector.inject(Object) . Это необходимо для того, чтобы добавить класс в граф зависимостей. Т.е. если у нас есть хотя бы один @Inject в классе — нам необходимо добавить этот класс к граф. В результате в нашем графе должны быть все классы, которые просят зависимости (каждый из этих классов должен сделать ObjectGraph.inject() ) + модули, которые удовлетворяют эти зависимости (обычно добавляются на этапе инициалзации графа).
Теперь вернемся к нашей изначальной задаче — протестировать все. Нам необходимо каким-то образом уметь подменять HttStack. За удовлетворение этой зависимости (хмм — только сейчас заметил как это интересно звучит) отвечает модуль NetworkModule:
@Provides @Singleton HttpClient provideHttpClient()
Один из вариантов — это добавить какой-нибудь конфигурационный файл, который будет диктовать какой environment использовать:
@Provides @Singleton HttpClient provideHttpClient() < if(Config.isDebugMode()) < return new MockHttpClient(); >return new OkHttpClient(); >
Но есть вариант еще элегантней. В Dagger можно создавать модули, переопределяющие функции, предоставляющие зависимости. Для этого в модуль надо добавить параметр overrides=true :
@Module(overrides=true, injects=TwitterApi.class) public class MockNetworkModule < @Provides @Singleton HttpClient provideHttpClient() < return new MockHttpClient(); >>
Все что остается сделать — это добавить этот модуль в граф на этапе инициализации:
Теперь все наши запросы будут идти через тестовый Http клиент.
Это далеко не все фичи Dagger’a — я описал только один из возможных сценариев использования данной библиотеки. В любом случае, без вдумчивого прочтения документации не обойтись.
//Entry point нашей программы public class TweeterApp < public static void main(String. args) < Injector.init(new NetworkModule()); Tweeter tweeter = new Tweeter(); tweeter.tweet("Hello world"); Timeline timeline = new Timeline("Test User"); timeline.loadMore(20); for(Tweet tweet: timeline.get()) < System.out.println(tweet); >> > //Инициализатор графа public class Injector < public static ObjectGraph graph; public static void init(Object. modules) < graph = ObjectGraph.create(modules); >public static void inject(Object target) < graph.inject(target); >> //Собственно, Tweeter (уже не принимающий HttpClient в конструкторе) public class Tweeter < private TwitterApi api; public Tweeter() < this.api = new TwitterApi(); >public void tweet(String tweet) < api.postTweet("Test User", tweet); >> //TwitterApi, который запрашивает HttpClient у Dagger'a public class TwitterApi < @Inject private HttpClient client; public TwitterApi() < //Добавляем класс в граф зависимостей Injector.inject(this); //На этом этапе "магическим" образом client проинициализирован Dagger'ом >public void postTweet(String user, String tweet) < HttpUrlConnection connection = client.open(". "); /* post tweet */ >> //Модуль, который предоставляет HttpClient всем, кто об этом просил (список "просящих" указывается в 'injects' параметре) @Module(injects=TwitterApi.class) public class NetworkModule < @Provides @Singleton HttpClient provideHttpClient() < return new OkHttpClient(); >>
Список полезных материалов по теме:
Dependency Injection on Android With RoboGuice
Ashraff Hathibelagal Last updated Sep 18, 2015
Introduction
RoboGuice, also called Google Guice on Android, is an easy-to-use dependency injection framework, which can make Android development more intuitive and convenient. Using this framework, you can drastically reduce the amount of code you write for performing common tasks, such as initializing various resources, accessing Android system services, and handling events.
In this tutorial, I will be showing you how to make the most of RoboGuice 3 in your Android projects.
1. Understanding Dependency Injection
Traditionally, if an object depends on something, it is its own responsibility to satisfy that dependency. In simpler terms, if an instance of class A depends on an instance of class B, then the developer is usually expected to call the constructor of class B inside the code of class A. This obviously leads to tighter coupling between the two classes.
Dependency injection is a design pattern in which objects rely on external code, which is commonly referred to as a dependency injector, to satisfy their dependencies. This means that if an object depends on other objects, it doesn’t have to know how to create or initialize those objects. This reduces the coupling between the objects and leads to code that is more modular, easier to modify, and less complex to test.
Thus, by using dependency injection, you can largely do away with constructors and factory methods in your project’s business logic.
2. How RoboGuice Works
Google Guice is a framework that makes it easy for you to create, configure, and use a dependency injector in your Java projects. RoboGuice builds on Google Guice and comes with a pre-configured dependency injector for Android. Simply put, out of the box, RoboGuice knows how to initialize various Android objects, get references to various resources of an app and more.
RoboGuice uses Java annotations, which are nothing but metadata embedded inside Java code, to determine what has to be injected where. Earlier versions of RoboGuice used to process annotations using the Java Reflection API during runtime and were often criticized for being slow. RoboGuice 3, however, comes with RoboBlender, a compile-time annotation processor that drastically improves RoboGuice’s performance.
3. Setting Up RoboGuice
Before you use RoboGuice, you must add it as a compile dependency in your app module’s build.gradle file. As it is available on Android Studio’s default repository, jcenter, doing so requires just one line of code.
compile 'org.roboguice:roboguice:3.0.1'
To improve the performance of RoboGuice, it is recommended that you also add RoboBlender, an annotation processor, as a provided dependency.
provided 'org.roboguice:roboblender:3.0.1'
To be able to use RoboGuice’s annotations in your Android activities, their classes must extend RoboActivity instead of Activity . Similarly, if you want to use the annotations inside an Android service, its class must extend RoboService instead of Service .
4. Associating Layouts With Activities
Normally, you would use the setContentView method and pass a layout resource to it in order to set the layout of an Activity . RoboGuice offers an alternative means to do the same thing, the @ContentView annotation.
For example, here’s how you would apply the layout defined in an XML file called activity_main.xml to a RoboActivity called MainActivity:
@ContentView(R.layout.activity_main)
public class MainActivity extends RoboActivity