過去の調査に加えて、Toothpickに潜ってみました。
簡単なまとめ
@injectアノテーションが付いている要素を、scope.getInstance();したクラスでinjectionして利用することができる。- inject可能な要素は木構造で表現され、親scopeに含まれる要素をinjectしたメソッド/クラスとして利用することができる
- scope外の要素を使おうとするとクラッシュ
bind(IFoo.class).to(Foo.class)でFoo.classを使い回すことができるし、bind(IFoo.class).to(new Foo())では都度新しいinstanceを生成して利用可能- ActivityやFragmentといったライフサイクルに対して
Toothpick.openScopes(getApplication(), this)してscopeを設定した場合、そのライフサイクルのonDestroyのタイミングでそのscopeをcloseする必要がある - Scopeはアノテーションベースでも独自で設定できる
導入
ライブラリとして利用するための導入は こちらのWiki に任せて省略。
例えば、以下のFooクラスに対してinjectしようとします。
class Foo {
@Inject Foo() {...}
@Inject Bar bar;
@Inject void setQurtz(Qurtz qurtz) {...}
}
この時、scope.getInstance(Foo.class) したら、そのクラス内でこの Foo.class でinjectされている Foo、Bar、setQurtz を @Inject で指定して使い回すことが可能となります。
//creating an instance in a scope Scope scope; scope.getInstance(Foo.class);
ついでに bind に関して少し触れておきます。以下のようにbindすると、
bind(IFoo.class).to(Foo.class); // case 1 bind(IFoo.class).to(new Foo()); // case 2
それぞれのケースにおいて以下のようになります。
- case1 では、すでに
Foo.classのインスタンスが存在するならそれを使い、それをIFoo.classに紐付ける - case2 では、常に新たな
Foo.classのインスタンスを使い、それをIFoo.classに紐付ける
Scopeの種類
Scopeの種類としては2つ存在します。
- a binding
- 都度、bindされるたびに新しいインスタンスとして利用される
- 子要素では親要素を上書き可能
bind(IFoo.class).to(Foo.class);みたいな感じで都度bindされるやつです
- scoped instances
- injectionされたscope内で使い回される、Singletonのようなインスタンス
- 子要素でも
Toothpick.openScope(getApplication(), this);のようにscopeとして設定されるやつです
ライフサイクルとの兼ね合い
Toothpickでは、Scopeという概念でinjectionする対象に木構造をもたせています。この構造を構築するために、 openScopes というメソッドが存在します。
Wikiはこちら
このopenScopesは、左=>右の順に親=>子という形でinjectする対象の関係を作ることができます。そのため、以下のような例の通り
Toothpick.openScopes(application, activity, fragment); Toothpick.openScopes(application, service1); ToothPick.openScopes(application, service2);
のようにすると、以下のような木構造が生成されます。
//Example of scopes during the life of an Android application
Application Scope
/ \ \
/ \ \
Activity Scope \ Service 2 Scope
/ \
/ Service1 Scope
/
Fragment Scope
これにより、 Activity Scope でinject出来るように指定されたものは、 Fragment Scope にだけ使うことができるようになります。Service1や2では使えません。使おうとした場合は クラッシュ します。
この構造を持つことで、injectionする対象の影響範囲を限定することができ、どこに存在するかわからないものをinjectするというような不安なことをする必要はなくなります。
この openScope の対になるものが closeScope です。このscopeはライフサイクルに応じてちゃんと閉じてあげる必要があります。閉じることで、そのクラスにinjectされる時に生成されたようなインスタンスはGCの対象になります。
例えば、Activityの onCreate で以下の通りした場合、onDestroy でそのActivityにinjectされたscopeをcloseします。
onCreate() {
Toothpick.openScope(getApplication(), this); // Applicationの子としてこのクラスを設定。Applicaitonに設定されたinjectを利用可能になる。
}
...
onDestroy() {
Toothpick.closeScope(this);
suprt.onDestroy();
}
少し手間かもしれませんが、AndroidでonCreate/onDestroyのライフサイクルを意識することは必要なので、このくらいであればさほど手間にはならないのでないかな、という感じです。
Custom scope
@javax.inject.Scope によって、独自にscopeを定義することができます。これにより、annotationベースでinjectionしたい対象をbindすることが可能になります。Wikiはここら辺
annotationを使うと以下のようなscopeが飛んだ関係性を作ることができる( @Singleton と @S2Singleton )のですが、これはtree構造の利点を享受できなくなるので、バットプラクティスとしているみたいです。柔軟な形を作ることはできるけれど、それは良くない反作用も得てしまうということですね。
Scope S0 = @Singleton //bound to the scope annotation @Singleton
\
\
Scope S1 // not bound to any scope annotation
\
\
@S2Singleton //a scope bound to the scope annotation @S2Singleton
Nevertheless, this is the way we can migrate apps that were using the infamous ContextSingleton of RoboGuice. But remember, this is a bad practice, as it doesn’t take advantage of scope resolution, or blurs it and make it leak-prone.
custom scopeの作り方なんかは実際のWikiなどを見ながらの方が理解しやすいと思うので、ここでは省略。この記事に出る @Inject 以外のアノテーション(@ActivitySingletonとか)はこの機能を使ったもの、と見てもらって大丈夫です。
bindingされた要素の生成
このWikiのところでは、bindingされたクラスがどのようなルールで扱われるかが書いていました。ここはざっと把握していた方が良さそう。
class SimpleModule extends Module {
SimpleModule() {
bind(IFoo.class).to(Foo.class); // case 1
bind(IFoo.class).to(new Foo()); // case 2
bind(IFoo.class).toProvider(FooProvider.class); // case 3
bind(IFoo.class).toProvider(new FooProvider()); // case 4
bind(Foo.class); // case 5
}
}
この時、case2と4は都度新しいインスタンスが生成され、その結果がIFoo.classにバインドされます。その他はすでに存在するインスタンスを使いまわします。そのため、case2とcase4ではインスタンスの破棄など、ちゃんと開発者が気をつける必要があります。
なので、以下のようなルールを敷くと良い、と書かれています。
As soon as Toothpick creates an object, its dependencies will be injected.
つまるとこ、
As soon as a developer creates an object with Toothpick, she has to take care of injecting this object dependencies.
とのこと。なるべくToothpickに任せましょう。
scope resolution
Wikiの子のページ
Toothpickでは木構造を常に上に遡ります。そのため、injectされた依存性も親方向だけを気にすれば良いです。
The core idea is that scopes are always bubbled up when resolving an injection, and looking up for a binding. Toothpick never goes down the scope tree.
Androidでは、なんだかんだでApplication classとかそん在するし、contextを意識したりすると縦横無尽に依存性を持たれると把握が困難になるのでこの制約はありなのかなと思います。
ここの実際はWikiを見る方が良いのですが、以下に例を載せます。
class DisplayImpl1 {@Inject Scope scope}
class DisplayImpl2 {@Inject Scope scope}
@Singleton class FooSingleton {@Inject IDisplay display; @Inject Scope scope}
@ActivitySingleton class FooActivitySingleton {@Inject Scope s; @Inject IDisplay display;}
@Singleton class FooSingletonError {@Inject FooActivitySingleton foo;}
この時、このクラスは以下のような関係を持ちます。
//Example of scopes during the life of an Android application +----------------------------------------------------------------------------------+ Resolution | +---------------------------------------------------------------+ | space | | application scope = @Singleton : | Resolution | for Activity | | / - Scope --> (application) | space | scope | | / - IDisplay --> DisplayImpl1 | for @Singleton | | | / - FooSingleton --> (FooSingleton) | scope | | +-----/---------------------------------------------------------+ | | activity scope = @ActivitySingleton : | | - Scope --> (activity) | | - IDisplay --> DisplayImpl2 | | - FooActivitySingleton --> (FooActivitySingleton) | +----------------------------------------------------------------------------------+
ここで、application/activityのscopeを見てみると以下な感じで観察できます。クラッシュするところだけ言葉を加えていますが、他はWikiを見てください。(省略している)
application scopeを対象- scope.getInstance(Scope.class)
- scope.getInstance(FooSingleton.class)
- scope.getInstance(FooActivitySingleton.class)
- scope.getInstance(FooSingletonError.class)
activity scopeを対象- scope.getInstance(Scope.class)
- scope.getInstance(IDisplay.class)
- scope.getInstance(FooSingleton.class)
- scope.getInstance(FooActivitySingleton.class)
- scope.getInstance(FooSingletonError.class) : これは唯一クラッシュする。
@Singletonでinjectされているので、activityのscopeに入っていないので。
このscopeの利点としてメモリリークなどの幾つかの事例をWikiのここでは載せています。が、ここでは省略。
Scoped & Unscoped Bindings
Bindingに関する話。wikiではここ
bindingには2種類存在します。
- unscoped bindings
- scoped bindings
これを、以下を例にして考えます。
class A {
@Inject IFoo foo1;
@Inject IFoo foo2;
}
class Foo {
@Inject Scope s;
}
これの違いは、bindされたスコープは、その影響がScope S2まで及ばない、ということです。
unscoped binding
bind(IFoo.class).to(Foo.class)
Scope s0 : Scope --> S0
\
\
Scope S1 : Scope --> S1 & IFoo --> Foo
\
\
Scope S2 : Scope --> S2
scoped binding
bind(IFoo.class).to(Foo.class).scope()
Scope s0 : Scope --> S0
\
\
Scope S1 : Scope --> S1 & IFoo --> (Foo) // <-- scoped binding
\
\
Scope S2 : Scope --> S2
これの実際のscopeは以下のようになります。これを見ても、確かにS2に影響を及ぼさない形になっていますね。(実際にテストコードなどで動かしてみると良さそう)
//space of creation of Foo instances in the case of a scoped binding in S1.
+--------------------------------------------------------------------+
| Scope s0 : Scope --> S0 |
| \ |
| \ |
| Scope S1 : Scope --> S1 & IFoo --> (Foo) // <-- scoped binding |
| \ |
+-----------\--------------------------------------------------------+
\
Scope S2 : Scope --> S2
scopedできる/できないは、以下のようにinjectする都度 new で新たなインスタンスを生成するかどうか、のようです。
bind(IFoo.class).to(Foo.class); // can be scoped bind(IFoo.class).to(new Foo()); // cannot be scoped (it is indeed scoped) bind(IFoo.class).toProvider(FooProvider.class); // can be scoped, will scope both the provider and produced instances bind(IFoo.class).toProvider(new FooProvider()); // cannot be scoped (the provider is scoped) bind(Foo.class); // can be scoped
factoryの生成
Toothpick will always inject all the dependencies (expressed by injected constructors, injected fields or injected methods) of all instances it creates.
Toothpickは、 @Inject を持っているClassに対して、Factoryを自動生成します。そのFactoryを最適化するよな挙動があるのですが、それが適用される/されない場合を書いています。最適化の説明はこちらを参考にということで、ここではパターンだけ並べておきます。
(私の備忘録というか、メモ込みで。)
Classes with @Inject annotated members (fields or methods)
public class Foo {
@Inject Foo() {...}
}
- for the class
Foo(optimistic) - for the class
Bar(optimistic)
Constructors with parameters
class Foo {
@Inject Foo(Bar bar);
}
for the class Foo (non optimistic factory, this is the normal case)
for the class Bar (optimistic)
Scope annotated class
@ActivitySingleton
class Foo {
}
- for the class
Foo(optimistic)
Factories and scopes
scoped annotationに対するFactoryはいかのように
//a scoped Factory
public final class Foo$$Factory implements Factory<Foo> {
@Override
public Foo createInstance(Scope scope) {
scope = scope.getParentScope(ActivitySingleton.class);
Bar bar = scope.getInstance(Bar.class);
return new Foo(bar);
}
}
Member Injection
public class Foo {
@Inject void m(Bar bar) {...}
}
//a Member Injector
public final class Foo$$MemberInjector implements MemberInjector<Foo> {
@Override
public void inject(Foo foo, Scope s) {
Bar bar = Toothpick.getInstance(Bar.class);
return foo.m(bar);
}
}
Factories & Member Injectors
public class Foo {
@Inject Bar bar; //an injected field
@Inject Foo() {...} //an injected constructor
}
//a simplified Factory
public final class Foo$$Factory implements Factory<Foo> {
@Override
public Foo createInstance(Scope scope) {
Foo foo = new Foo();
new Foo$$MemberInjector().inject(foo, scope);
return foo;
}
}
Member Injectors and inheritance
class Foo {
@Inject Bar bar;
}
class FooChild extends Foo {
@Inject Qurtz qurtz;
}
- この場合は、
FooChildがFooのmember injectorを使うようです。つまり、FooChildはFooChildのmember injectorと、Fooのmember injectorを持つことになる模様。
締め
ざっと、ToothpickのWikiとサンプルコードを動かしながら、Tree Based DIのコンセプトと使いかたを学んでみました。
AndroidのDIではDagger2が最近では多いと思います。一方で、個人的にはDagger2を学んだ時には頭に馴染むまでに少し時間がかかりました。
このToothpickはAndroidのContextに沿った形でも考えられていて、個人的にDagger2を学ぶよりは学習は低かったです。個人的には、Androidアプリという文脈だとこのTree Based DIもありなのではないかなと感じます。
テスト機構も提供されているのが個人的にはGoodでした。
Dagger2との速度比較では、injectするメソッド数が1000超えないくらいだと大差ないので、使いやすさ次第になりそうですね。どうだろう。完全なAndroiderの人の反応が気になるところ。