この記事は AdventCalendar Android その2 の9日目の記事です。
Dependency Injection(以下、DI)についてのお話をします。
細かい話はいろいろな所で既にたくさんありますので、
「とりあえずAndroidでDIを取り入れたい」という方向けに簡単にご説明しようと思います。
厳密には正しくない話も紛れていますがご了承下さい。
#DIとは?
英語でDependency Injection、よく和訳で「依存性(の)注入」とか言われるデザインパターンです。
「依存性を注入する」というと何やらヤバげな匂いがしますが、
「依存性」とは、もうすこし簡単に言うと「(依存関係にある)オブジェクト」と言い換えられます。
つまり「依存関係にあるオブジェクトは外部から注入するようなデザインパターン」です。
もう少し具体的にご説明しようと思います。
class Main{
Client().methodB()
}
class Client{
fun methodB(){
val service: Service = Service() // Serviceを生成
service.methodA() // Serviceを利用
}
}
class Service{
fun methodA(){
...
}
}
上記の例はDIパターンを用いていません。
Clientクラスの中でServiceオブジェクトを生成し、methodAを呼び出しています。
これを、「ClientクラスはServiceクラスに依存している」とか言ったりします。
実際にはこれでもきちんと動作しますし、このような記法はいたるところで頻繁に利用されています。
問題点なんかは後述するとして、ひとまず簡単にDIっぽく書き直してみます。
DIの手法にも色々とあるのですが、とりあえずは「コンストラクタDI」というものを使ってみます。
コンストラクタDIとは、依存性(オブジェクト)をコンストラクタを経由して注入する手法です。
class Main{
val service = Service() // Serviceを生成
val client = Client(service) // Serviceを注入
client.methodB()
}
class Client(val service: Service){
fun methodB(){
service.methodA() // Serviceを利用
}
}
class Service{
fun methodA(){
}
}
では最初の、DIを用いないコードは何が問題なのでしょうか?
一つあげるとすると、Serviceの修正時に影響範囲が大きくなってしまうことにあります。
例えば、Service内に予めプロパティをセットするためにコンストラクタを修正してみます。
class Service(prop: String){
fun methodA(){
println(prop)
}
}
この場合、DIで設計されていない方ではClientの中でServiceインスタンスを生成している箇所に
コンストラクタ引数を渡すようにしてあげる必要があります。
すると、Clientの呼び出し元の一つであるMainクラス以外でもClientを呼び出している場合、
全ての箇所で今回の修正に伴う影響が発生してしまいます。
ではDIだとどうかというと
class Main{
val prop = "test"
val service = Service(prop)
val client = Client(service)
client.methodB()
}
このように呼び出し元であるMainクラス側でServiceの制御が可能となるため、Clientの修正が不要になります。
すると他のところでClientを呼び出している場合でも、それぞれの呼び出し元で影響を考慮すればよいことになります。
##抽象に依存せよ
もう一点、DIパターンに限らずオブジェクト間の結合を疎にする手法として「抽象に依存せよ」という言葉があります。
いままではクラス名を直接指定してオブジェクトを生成していましたが、
これだとスタブを利用して実装を行うときやテスト用のモックを利用したい時に、
Clientクラスのコンストラクタの型を書き換えてあげないといけません。
そうなるとテストももちろん大変になりますが、リリース時に直し忘れていたりすると・・・更に大変ですね。
ここでいう「抽象」とは、インターフェースのことを指すと思っていただいてOKです。
今回はIServiceインターフェースを作成し、ServiceクラスにIServiceインターフェースを実装させるようにします。
そして、今まで具体的にServiceのクラス名を明示していたClientクラスのコンストラクタをIServiceに書き換えます。
val test= true
class Main{
val service = if(!test) Service() else StubService() // ServiceA or Bを生成
val client = Client(service) // ServiceA or Bを注入
client.methodZ()
}
class Client(val service: IService){
fun methodZ(){
service.methodX() // ServiceA or Bを利用
}
}
interface IService{
fun methodX()
}
class Service: IService{
fun methodX(){
...
}
}
class StubService: IService{
fun method X(){
...
}
}
こうなると、例えばServiceクラスの実装がまだでも、とりあえずStubServiceを実装しておけば以後の開発が進められますね。
また、下流のテストを実施したい場合も同様です。
まとめてしまうと、
- オブジェクトの「生成&注入」と「利用」を分離せよ
- 抽象(インターフェース)に依存せよ
この2点を意識するようにすれば、それなりにDIっぽいものは書けているはずです。
今回はコンストラクタDIというものを利用しましたが、他のパターンもあります。興味が湧いたら調べて見て下さい。
AndroidでのDI利用パターン
さて、DIの説明だけでだいぶ尺を取ってしまいましたが、今回はAndroid Advent Calenderということで、
AndroidでのDI利用パターンについてもお話しないといけませんね。
上記で触れたような簡易的なDIデザインパターンで小規模なコードであれば必ずしも必要ではないのですが、
DI実装を楽にするためにDIコンテナというものを利用することができます。
(どんどん実装が大きくなると、上記のコンストラクタDIの場合、引数の数がどんどん増えてしまいますよね)
DIコンテナを利用することで、引数の数や種類が変わったりしても元のソースコードではなく、
DIコンテナ側を修正することで対応できるようになり、保守性が上がります。
Androidで利用可能なDIコンテナ(ライブラリ)として有名なのは以下のようなものでしょうか。
しかし上記の3つはいずれも javax.inject.Inject
アノテーションを利用します。
・・・が、私はアノテーションが嫌いです。
特にKotlin + Daggerでプロジェクトを始めようとしてググった結果最初に出てくるような、
ウェブサイトの言う通りに実装すると間違いなくDIライブラリのアノテーション絡みのコード自動生成がうまくいきません。
なので私は
というDIコンテナをよく利用します。
Kodein
Kodeinを使うメリットとして公式には以下の通りの記載があります。
- JVM上/Android上/javascript上で利用可能
- 必要になった時に依存性の遅延読み込みを行う
- オブジェクトの初期化の順序を気にする必要がない
- クラス/インターフェースをインスタンス/プロバイダに容易にバインドできる
- オブジェクトのバインドや再帰を容易にデバッグできる
- 軽量で高速に動作し、最適化も行われている
- 可読性の高い宣言的プログラミングを実現することができるよう設計されている
- Androidとの統合性が高い
- Kotlin風の文法で利用可能なAPIを提供している
- もちろん普通にJavaでも利用可能
そして何より、javax.inject.Inject
を始めとするアノテーションプロセッシングを利用しないため、私のようなアノテーション嫌いにも最適です。
インストール
簡単なのはGradleかMavenで以下の通りです。
<dependency>
<groupId>com.github.salomonbrys.kodein</groupId>
<artifactId>kodein</artifactId>
<version>4.1.0</version>
</dependency>
compile 'com.github.salomonbrys.kodein:kodein:4.1.0'
依存性の宣言
たとえば、文字列をインスタンスに注入するために、ここでバインドしてあげます。
どこでもいいのですが、とりあえずどこでも簡単に拾えるようにApplicationクラスのサブクラスとしてみましょう。
class MyApp: Application(), KodeinAware {
override val kodein = Kodein {
bind<String>() with instance("This is binded string.")
}
}
依存性解決
class MainActivity : KodeinAppCompatActivity() {
val message: String by instance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(message)
}
}
この val message: String by instance()
により、message変数に文字列が注入されます。
何が嬉しい?
上記の例だと1つしか依存性の注入を行っていないのでわかりにくいのですが、
1つのKodeinブロックの中で複数の依存性の宣言を行うことももちろん可能です。
その場合、どの順番で宣言や注入を行うかを考える必要がなく、
with XXX
という記法に従いKodeinライブラリが自動で決定してくれます。
また、ここに依存性を追加したいと思った時に、例えばコンストラクタDIを手で実装すると、
コンストラクタに引数を追加して、その受け渡し部分を修正して、とやや手間がかかりますが、
Kodeinを利用すると Kodein{ }
内に依存性の宣言を1つ追加し、
利用したいところではby XXX()
で依存性を注入することができるようになります。
逆に、注入する必要のない依存性については、その注入を省くこともできるため、
Service側の実装にあわせて、そのServiceに依存する全てのClientを修正する必要もなくなります。
#まとめ
DIは結合度を低下させ、アジャイル開発や単体テストに対し強いメリットを持っています。
小難しい用語や定義は抜きにして、まずは「それっぽいもの」を1つ作ってみると、
だんだんと何が便利なのかがわかってくるのではないかなと思います。
ぜひAndroid開発にもDIを活用してみて下さい!