
この記事では、成長しているオープンソースのDaggerライブラリの例により、Androidでの依存性注入(だけでなく)に対処しようとします。
それでは、依存性注入とは正確には何ですか? ウィキペディアによると、これはコード内の依存関係を動的に記述し、ビジネスロジックをより小さなブロックに分割できる設計パターンです。 これは、最初にこれらの同じブロックをテストブロックに置き換えて、テスト領域を制限できるという事実により、まず便利です。
洗練された定義にもかかわらず、原則は非常にシンプルで些細です。 ほとんどのプログラマは何らかの形でこのパターンを実装していると確信しています。
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!");
それは簡単だと思われるでしょうか? 実際、依存性注入パターンを「手動で」実装しました。 しかし、1つの「しかし」があります。 最後のn個のメッセージをロードできるTimelineクラスがある状況を想像してください。 このクラスは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は、okhttp、retrofit、picasso、および多くのAndroid開発者に知られている他の多くの優れたライブラリからのオープンソースの依存性注入ライブラリです。
Daggerの主な利点(同じGuiceと比較):
- すべての依存関係の静的分析
- コンパイル段階での構成エラーの検出(実行時だけでなく)
- 構成プロセスを大幅に高速化する反射の欠如
- かなり少ないメモリ負荷
Daggerでは、依存関係の構成プロセスは3つの大きなブロックに分割されます。
- 依存関係のグラフの初期化(ObjectGraph)
- 依存関係リクエスト(
@Inject
) - 依存関係の満足度(
@Module
/@Provides
)
依存関係をリクエストする
Daggerにフィールドの1つを初期化するよう要求するには、
@Inject
アノテーションを追加するだけです。
@Inject private HttpClient client;
...そして、このクラスがディペンデンシーグラフに追加されていることを確認します(詳細は後ほど)
依存関係を提供する
どのクライアントインスタンスを作成するかを短剣に伝えるには、「モジュール」(注釈付きの
@Module
クラス)を作成する必要があります。
@Module public class NetworkModule{...}
このクラスは、アプリケーションが要求する依存関係の「満足」部分を担当します。 このクラスでは、いわゆる「プロバイダー」を作成する必要があります
@Provide
インスタンスを返すメソッド(注釈
@Provide
):
@Module(injects=TwitterApi.class) public class NetworkModule { @Provides @Singleton HttpClient provideHttpClient() { return new OkHttpClient(); } }
Daggerに
@Inject
アノテーションを介してHttpClientを要求した人のためにOkHttpClientを作成したことを伝えました
コンパイル時の検証が機能するためには、この依存関係を要求するすべてのクラスを(injectsパラメーターで)指定する必要があることに注意してください。 この場合、TwitterApiクラスのみがHttpClientを必要とします。
@Singleton
は、Daggerに1つのクライアントインスタンスのみを作成してキャッシュするように指示します。
カウント作成
それでは、グラフの作成に移りましょう。 これを行うために、1つ(または複数)のモジュールでグラフを初期化する
Injector
クラスを作成しました。 Androidアプリケーションのコンテキストでは、アプリケーションを作成するときにこれを行うのが最も便利です(Applicationから継承し、
onCreate()
をオーバーロードし
onCreate()
)。 この例では、残りのコンポーネント(TweeterおよびTimeline)を含むTweeterAppクラスを作成しました
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)
注意してください。 これは、依存関係グラフにクラスを追加するために必要です。 つまり クラスに少なくとも1つの
@Inject
がある場合、このクラスをグラフに追加する必要があります。 その結果、グラフには依存関係を要求するすべてのクラスが存在するはずです(これらの各クラスは
ObjectGraph.inject()
を作成する必要が
ObjectGraph.inject()
ます)+これらの依存関係を満たすモジュール(通常はグラフの初期化段階で追加されます)。
元のタスクに戻ります-すべてをテストします。 どういうわけかHttStackを置き換えることができる必要があります。 NetworkModuleモジュールは、この依存関係を満足させる責任があります(うーん-それがおもしろいことに気付きました)。
@Provides @Singleton HttpClient provideHttpClient() { return new OkHttpClient(); }
1つのオプションは、使用する環境を指定する何らかの構成ファイルを追加することです。
@Provides @Singleton HttpClient provideHttpClient() { if(Config.isDebugMode()) { return new MockHttpClient(); } return new OkHttpClient(); }
しかし、さらにエレガントなオプションがあります。 Daggerでは、依存関係を提供する関数をオーバーライドするモジュールを作成できます。 これを行うには、モジュールに
overrides=true
を追加し
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のすべての機能ではありません-このライブラリを使用するための可能なシナリオの1つだけを説明しました。 いずれにせよ、ドキュメントを注意深く読むことは不可欠です。
結果は次のとおりです(上記と同じですが、ヒープにまとめられます)。
//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(); } }