rakuishi.com

依存性注入(DI: Dependency Injection)と Dagger 2

依存性注入(DI: Dependency Injection)という単語を知ってはいたけれど、その意味を知らなかったので調べた。また、前に書いた Todo-Android という Android アプリを元に依存性注入を考えた。

Android では、Dagger というライブラリが DI を実装するのに有名みたいだ。Square 製の Dagger があり、それをフォークした Google 製の Dagger 2 がある。開発が盛んである Dagger 2 を使った。

依存性注入すると何が解決できそうか

Todo-Android アプリの中では、RealmTodoManager が Todo 情報を管理していて、Todo リストを返したり、Todo の追加・更新・削除を担っている。

public class RealmTodoManager {
    public RealmTodoManager() { /* 省略 */ }
    public Todo find(int id) { /* 省略 */ }
    public List<Todo> findAll() { /* 省略 */ }
    public void insert(String name, boolean completed) { /* 省略 */ }
    public void update(int id, String name, boolean completed) { /* 省略 */ }
    public void delete(int id) { /* 省略 */ }
}

この RealmTodoManager は、各 Activity でインスタンス化し、使い回している。各 Actiivty は、RealmTodoManager に依存している状態である。

ここで開発の途中にデータ管理を Realm から SQLite に変えた時は、SQLiteTodoManager を作成することになるが、各 Activity を全部書き直すことになる。あるいは、Debug ビルドの時は、モックデータを返すようにしたい時に、どのように実装すれば良いのか悩むことになる。

また、RealmTodoManager をシングルトンにしたい時に各 Activity で書き換えが必要になるし、シングルトンはテストがしにくいから、なるべくなら避けたい気がする。

依存性注入を行うと、このようなもやもやがうまく解消できるようだ。

Dagger 2 による依存性注入の手順

Gradle 設定

トップレベルの build.gradle ファイル(Project)に、android-apt の設定を追加する。

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.1.0'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4' // 追加
    }
}

/* 省略 */

アプリケーションの build.gradle ファイル(Module: app)に、以下の設定を追加する。

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt' // 追加

/* 省略 */

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'javax.annotation:jsr250-api:1.0' // 追加
    compile 'com.google.dagger:dagger:2.0.1' // 追加
    apt 'com.google.dagger:dagger-compiler:2.0.1' // 追加
}

実装

まずは、TodoManager というインターフェースを宣言する。

public interface TodoManager {
    Todo find(int id);
    List<Todo> findAll();
    void insert(String name, boolean completed);
    void update(int id, String name, boolean completed);
    void delete(int id);
}

このインターフェースを実装した RealmTodoManager を作成する。今後、SQLiteTodoManager を作るときもこのインターフェースを実装する。同様にモックデータを返す MockTodoManager とかも作れる。

public class RealmTodoManager implements TodoManager {
    public RealmTodoManager(Context context) { /* 省略 */ }
    @Override Todo find(int id) { /* 省略 */ }
    /* 省略 */
}

モジュールを作成する。モジュールは、インスタンスを提供(provide)するメソッド群を定義したものである。メソッド名には provide を接頭語にする、メソッドは @Provides アノテーションを宣言するのが決まりとなっている。

provideTodoManager() は、TodoManager を提供する関数。この返り値の型は、TodoManager としているが、実際には RealmTodoManager のインスタンスを返している。今後 SQLiteTodoManager に差し替わった時には、ここの記述を差し替えるだけで良くなる。シングルトンにするかどうかも @Singleton アノテーションにより宣言できる。RealmTodoManager 側でシングルトンの設定をする必要がなくなるため、テストコードが書きやすくなる。

@Module
public class PersistenceModule {
    @Provides @Singleton
    public TodoManager provideTodoManager() {
        return new RealmTodoManager();
    }
}

次にコンポーネントを作成する。これは依存性を注入する先と、モジュールの関係性を定義する。ここでは、PersistenceModule が MainActivity に注入できるように宣言する。modules と複数形になっていることから察せられるように、複数のモジュールを紐付けることができる。

@Singleton
@Component(modules = PersistenceModule.class)
interface PersistenceComponent {
    void inject(MainActivity activity);
}

実際に利用するには、MainActivity の onCreate() 内などで inject する。この時に、@Inject と書かれたメンバ変数に provide で記述されたものが注入される。これを「依存性が注入された」と言うみたいだ。ポイントとしては、注入する実態が MainActivity 側から取り除かれているということ。

public class MainActivity extends BaseActivity {
    @Inject TodoManager mTodoManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        PersistenceComponent component = DaggerPersistenceComponent.create();
        component.inject(this);
    }
}

ちなみに DaggerPersistenceComponent だが、これは PersistenceComponent のビルド後に作成される。

参考