Skip to content

Instantly share code, notes, and snippets.

@numa08
Last active March 7, 2018 06:42
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save numa08/894c4889badb1c8d97712dcf0de7fcec to your computer and use it in GitHub Desktop.
Save numa08/894c4889badb1c8d97712dcf0de7fcec to your computer and use it in GitHub Desktop.
why dagger2

どうして Dagger を利用するのか、同じことは setter を実装して外部から利用するインスタンスを変更することで実現できるのではないのか?

結論から言うと Dagger を利用した依存性の解決(注入)を実現することが楽だからです。

そもそも、広い意味での DI の実現方法には setter を利用した DI が存在します。setter injection と言うそうです。

以前の勉強会のコード を setter injection を利用して実装すると、だいたい次のようになると思います。

setter injection による実現

public class MainActivity extends Activity {

  private Today today;
  public void setToday(Today today) {
    this.today = today;
  }

  @Override
  protected void onResume(){
    Log.d("Android_Test_BC", showHour());
  }

  String showHour() {
    return "今は" + today.getHour() + "時です";
  }

}
public class CustomApplication extends Application {

  @Override
  public void onCreate() {
    super.onCreate();
    registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks {
      @Override
      public void onActivityCreated(Activity activity, Bundler savedInstanceState) {
        if (activity instanceof MainActivity) {
          ((MainActivity) activity).setToday(new TodayImpl());
        }
      }
      // 他のメソッドは省略
    })
  }
}
@RunWith(JUnit4.class)
public class MainActivityTest {

  static class MockToday implements Today {
    @Override
    public int getHour() {
      return 12;
    }
  }

  @Test
  public void showHour() throws Throwable {
    final MainActivity activity = new MainActivity();
    activity.setToday(new MockToday());
    final String hour = activity.showHour();
    assertThat(hour, is("今は12時です"));
  }

}

無事にもとのコードとだいたい同じことが実現できました。ただし、このコードには問題があります。

それは、 CustomApplication の実装です。 MainActivitysetToday を呼び出すために、 CustomApplicationActivityLifecycleCallbacks を利用しています。 ActivityLifecycleCallbacks の中では instanceof 演算子を利用して、 MainActivity を判断し setToday を呼び出しています。この実装は人の目で判断しなければならない箇所です。このサンプルでは1つの Activity に対してのみ instanceof を利用していますが、アプリの拡張に伴って複数の ActivityFragment を利用する場合は多くあります。 そうなってくると、「必要なインスタンスを set し忘れる」とか言ったミスが発生する場合があります。

Dagger2 はアノテーショを利用してコンパイル中に依存性の解決を行うコードを自動生成します。そのため、必要なインスタンスの生成やセットを忘れていたとしてもコンパイルエラーが発生して、人間によるミスが起きる可能性は低くなるといえます。

まとめると、 Dagger2 を利用するモチベーションは次の2点です

  • setter injection を実現した場合、コードが増えてくるとミスが起こりがちになるため
  • コードの自動生成が行われるので、人が書くべきコードの量は少なくなって、DIの実現が楽になるため

Dagger2の公式ドキュメントにも

Dagger 2 is the first to implement the full stack with generated code. The guiding principle is to generate code that mimics the code that a user might have hand-written to ensure that dependency injection is as simple, traceable and performant as it can be.

とあるので、コードが自動生成される部分が大きなメリットと言えると思います。

res1

ライフサイクルコールバックを利用せず、 MainActivityonCreatesetToday をすれば良いのでは?

今回のサンプルではたしかにそうです。しかし、ライフサイクルに関するテストを行おうとすると、onCreatesetTodayする方法ではテストができなくなります。

例えば、Espressoを利用したUIテストを実行するケースですね。onCreatesetTodayをしてしまうと、テスト実行時でも常にTestImplのインスタンスを利用してしまうことになります。

Dagger2 を利用することで、テストのときにはテスト用のインスタンスを利用するように Module を調整することができるため、ライフサイクルに依存するケースであってもテストができるようになります。

また、このサンプルでもライフサイクルコールバックをテストコードとproductionコードで分けることでテスト時とアプリ実行時で利用するインスタンスを変更できるようになるはずです。

@junkuvo
Copy link

junkuvo commented Mar 7, 2018

すみません、こちらに記載した方がよかったですね。
Activityの起動(ライフサイクルでいうとonCreateから)の段階からすでにテスト用の状態に切り替わっている、という点ですね。
たしかに後からセットしてもできないことがありますね。

色々な方のサンプルで、Applicationクラスを継承したクラス(上でいうCustomApplication)にてcomponentをメンバ変数として保持し、各アクティビティでそのcomponentを利用するようなものが多く、この点も疑問でしたが、Applicationクラスのconponentの段階でテスト用のものに差し替えれば、ActivityをonCreateする段階ですでにテスト用に切り替わっている、ということが実現できそうだな、と思いました。
ありがとうございます、すっきりしました。実際もっと使って見て実感したいです。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment