Знакомимся с Dependency Injection на примере Dagger

от автора

http://radiant--eclipse.deviantart.com/
В данной статье мы попытаемся разобраться с Dependency Injection в Android (и не только) на примере набирающей популярность open source библиотеки Dagger (http://square.github.io/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()) { 	System.out.println(tweet); } 

Наш класс выглядит примерно так:

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<Tweet> get(){/*.......*/} } 

Вроде бы все ничего — мы применили тот же подход, что и с классом Tweeter — дали возможность указывать Http клиент при создании объекта, что позволяет нам протестировать этот модуль, не завися при этом от сети. Но! Вы заметили, сколько кода мы продублировали и как нам приходится «протаскивать» Http клиент прямо из «головы» приложения? Конечно, можно добавить конструкторы по умолчанию, которые будут создавать реальный Http клиент, и использовать кастомный конструктор только при тестировании, но ведь это не решает проблему, а только маскирует ее.

Давайте рассмотрим как мы можем улучшить сложившуюся ситуацию.

Dagger

Dagger — это open source Dependency Injection библиотека от разработчиков okhttp, retrofit, picasso и многих других замечательных библиотек, известных многим Android разработчикам.

Главные преимущества Dagger (по сравнению с тем же Guice):

  • Статический анализ всех зависимостей
  • Определение ошибок конфигурации на этапе компиляции (не только в runtime)
  • Отсутсвие reflection, что значительно ускоряет процесс конфигурации
  • Довольно небольшая нагрузка на память

В Dagger процесс конфигурации зависимостей разбит на 3 больших блока:

  • инициализация графа завизимостей (ObjectGraph)
  • запрос зависимостей (@Inject)
  • удовлетворение зависимостей (@Model/@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); 		this.client = client; 	}  	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() 	{ 		return new OkHttpClient(); 	} 

Один из вариантов — это добавить какой-нибудь конфигурационный файл, который будет диктовать какой 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(); 	} } 

Все что остается сделать — это добавить этот модуль в граф на этапе инициализации:

public class TweeterApp { 	public static void main(String... args) 	{ 		Injector.init(new NetworkModule(), new MockNetworkModule()); 		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); 		} 	} } 

Теперь все наши запросы будут идти через тестовый Http клиент.

Это далеко не все фичи Dagger’a — я описал только один из возможных сценариев использования данной библиотеки. В любом случае, без вдумчивого прочтения документации не обойтись.

Список полезных материалов по теме:

  • Описаниe Dependency Injection (http://en.wikipedia.org/wiki/Dependency_injection)
  • Сайт проекта (http://square.github.io/dagger/)
  • Google Hangout видеозапись с разработчиками библиотеки (http://www.youtube.com/watch?v=3mGf6b6JF00)
  • Мой маленький пример использования библиотеки (https://github.com/paveldudka/dagger-otto-demo.git)
  • Слайды с презентации Jake Wharton на Devoxx конференции (видео он пообещал выложить позже) (https://speakerdeck.com/jakewharton/android-apps-with-dagger)

ссылка на оригинал статьи http://habrahabr.ru/post/202866/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *