皆さん、こんにちは。kitakkunと申します。
「Dependency Injection(依存性注入)」って知ってますか?
この記事は、このDIを「完全に理解した」状態になるのが目標です。
誤解しないで欲しいのですが、「完全に理解した」は文字通り完全に理解することを言っているのではなくて、まずは最初の山場を超えてしまいましょうという話です。
Daggerに関しては色々調べて試行錯誤してこうしたら動いたよくらいの理解なので、一部誤っているかもしれません。識者の方、遠慮なくコメントでご指摘願います。
実は皆さん、既にDI経験者です
実はですね。ある程度の期間プログラミングをやっていれば、大抵DIは無意識のうちにやっているはずです。クラスやモジュールを分けながらプログラミングをしていると必然的にDIしたくなってきます。それでは、説明します!
(読んでる途中で、「いや、そんなのやってないよ!」と思った方も、「そうなんだ」くらいに読んでいただけると嬉しいです)
RPGゲームのキャラクタークラスを例に考えてみる
今皆さんは、RPGゲームを作っています。ゲーム内で戦闘を行うキャラクターはすべてCharacter
クラスで定義されているとしましょう。(open
はkotlinにおいて継承を許可する修飾子です)
open class Character {}
Character
は、防具(Armer
)と武器(Weapon
)を装備することができます。これらによって防御力や攻撃力などのパラメータ、そして攻撃モーションなどが決定するとします。
open class Armer {}
open class Weapon {}
繰り返しになりますが、Character
にはArmer
とWeapon
を装備できます。Character
がArmer
とWeapon
を持っていると考えたら、以下のような設計が考えられます。内部にメンバとして定義してしまう、というものです。
open class Character {
val armer = Armer()
val weapon = Weapon()
}
ここで、一つ問題が生じます。キャラクターは自由に装備を変更できるとしたら・・?
、、、ああ、わかりますよ。こうすればいいんでしょう?
open class Character {
var armer = Armer()
var weapon = Weapon()
}
val character = Character()
character.armer = MukiMukiArmer()
character.weapon = MoriMoriShord()
まあ確かに、こうすれば装備変更は可能だけど、、。後から代入するの忘れたときとか、armer
やweapon
が直接置き換えられるようになってて危険じゃない?
その上、キャラクターを作るときに必ず防具と武器が必要っていうんなら、作るときに渡してあげるのが自然じゃないかな。じゃあ、こう?
open class Character (val armer: Armer, val weapon: Weapon) { }
いいですね。これなら色んな武器や防具を渡して、様々なキャラクターを作れて便利そうです。
・・・ はい。これが、Dependency Injectionです。・・・ええ?「なるほど、わからん」ですか。次節でもうちょっと詳しく説明します。
一体どこがDIなの?
凄く当たり前にやることなので、気付かない方も多いかもしれませんが、DIはとても身近な存在なのです。
先程の例について、詳しく説明します。
キャラクターを作るときに必ず防具と武器が必要っていうんなら、作るときに渡してあげるのが自然じゃないかな。
と、わざと太字にしました。これ、難しく言うと次のように表現できます。
Character
クラスのインスタンス化にArmer
とWeapon
が必要なら、Character
クラスのコンストラクタに渡してあげるのが自然じゃないかな。
もっと行きましょう。
Character
クラスがArmer
とWeapon
に 依存している なら、コンストラクタで 注入してあげる のが自然じゃないかな。
おや、「依存している」という表現が出てきました。また、「注入してあげる」という言葉も出てきました。
「依存」と「注入」・・・? 依存性注入・・・?? これ、DIやーーーーん!!
つまり、そのクラスを使うために必要なクラスを、内部で作るのではなく外部から注入してあげるというのが、Dependency Injectionです。どうですか?「完全に理解」しましたか?
DIすると何が嬉しいのか
一般にDIをすると、テストが簡単になると言われています。でも、個人レベルでプログラミングしていると、あまりテストは書かないし、利点を感じにくいかもしれません。
そのため、先程の例のように、内部で生成してしまうと柔軟性が損なわれるんだな、くらいの理解で最初は大丈夫だと思います。DIする前は防具と武器が固定で困ってましたけど、DIしたら色んな防具と武器をキャラクターに装備させられるようになりましたね。
DIすると何が残念なのか
DIすると、インスタンスの生成がちょっと面倒になります。なぜなら、先程の例で言えば、DIする前は
val character = Character()
でキャラクターを作れていたのに、DIした後は
val armer = Armer()
val weapon = Weapon()
val character = Character(armer, weapon)
としてインスタンス化しなければなりません。毎回これをやるのは非常に面倒くさいですね。今回の場合は多分渡すものが毎回変わる類のものかもしれませんが、渡すものが決まっている場合とかはなおさら面倒くさいです。
渡すものが決まっている、というのは例えばデータベースへアクセスするためのDatabase
クラスがあるとして、メッセージ投稿を担うクラスPostCreator
がこのDatabase
クラスに依存しており、DIした場合などです。
class PostCreator (private val database: Database) {}
勝手に突っ込んでくれよ!! と、思いませんか。はい。実は勝手に突っ込んでくれるツールがあります。皆さんご存知かもしれません。「Dagger」です。
Daggerは難しそうだけど、難しくない
DIを自動化するツールとして、Google社の提供する「Dagger」があります。
このDaggerはよく、難しすぎるだとか言われ敬遠されがちなのですが、やっていること自体はそんなに難しくはありません。ちょっと難しく感じるのは、見慣れないアノテーションをはじめ、コンポーネントやモジュールといった特殊な概念のせいです。
ここでは、Daggerを完全に理解するために丁寧にセットアップ方法を説明していきます!(すみません、daggerの使い方にフォーカスしたいのでbuild.gradle
周りの設定とかは割愛します)
@Inject
による依存性注入箇所の指定
Daggerにおいて依存性注入を行いたい箇所には@Inject
アノテーションをつけます。例えば、先程の例ならば
open class Character @Inject constructor(val armer: Armer, val Weapon) {}
といった具合になります。ちなみに、constructorに@Inject
をつけたときの依存性注入のことをコンストラクタインジェクションと言います。
一方で、最初に実は内部フィールドに防具や武器を定義するという実装を行っていましたが、コンストラクタで渡す以外にもう一つインスタンス化の後データをはめ込むタイプのインジェクションもあります。
open class Character {
var armer = Armer()
var weapon = Weapon()
}
val character = Character()
character.armer = MukiMukiArmer()
character.weapon = MoriMoriShord()
これですね。これを@Inject
を用いてやるならば、フィールドに@Inject
をつけ、フィールドをlateinit var
にします。
open class Character {
@Inject lateinit var armer: Armer
@Inject lateinit var weapon: Weapon
}
この手法は、フィールドインジェクションといい、コンストラクタ呼び出しがシステムにより行われ、コンストラクタインジェクションができない場合などに利用されます。
@Module
による注入データの定義
前節ではCharacter
クラスの依存性注入対象を、@Inject
を用いて行いました。次に、該当の型のインスタンスを自動で供給するためのモジュールを作成します。モジュールとなるobject
またはclass
には@Module
アノテーションをつけます。
@Module
object MyModule {}
では、Armer
とWeapon
を供給(@Provides
)するための関数を定義します。
@Module
object MyModule {
@Provides
fun provideArmer(): Armer = Armer()
@Provides
fun provideWeapon(): Weapon = Weapon()
}
これで、@Inject
アノテーションの付いたArmer
型、およびWeapon
型の変数には自動でそのインスタンスが注入されます。
補足: プログラム中で1つしかインスタンスを持たない、シングルトンのクラスを注入したい場合は
@Provides
に合わせて@Singleton
アノテーションをつけます。@Module object MyModule { @Provides @Singleton fun provideSomeSingleton(): SomeSingleton = SomeSingleton() }
ここまででとりあえずCharacter
クラスのインジェクションは終わりです。しかし、コンストラクタインジェクションの方は正常に動きますが、フィールドインジェクションの方は上手く動いてくれません。フィールドインジェクションを機能させるために必要な、「コンポーネント」について次節では説明したいと思います。
@Component
によるフィールドインジェクション
フィールドインジェクションは、モジュールだけでは完結しません。別途コンポーネントを作成し、該当のクラスで明示的にインジェクトしてやる必要があります。ただ、基本的にコンストラクタインジェクションで完結するので、こちらのコンポーネントに関しては使う機会が限られているかと思います。しかし、必要になるかもしれないので一応軽く説明します。
Character
クラスをフィールドインジェクションで実装したとしましょう。
open class Character {
@Inject lateinit var armer: Armer
@Inject lateinit var weapon: Weapon
}
これに注入するイメージで進めていきます。
コンポーネントのインタフェースを定義します。(インタフェースを定義すればあとは内部で実装部や自動でやってくれます)
@Component(modules = [MyModule::class])
interface MyComponent {}
@Component
アノテーションは引数にモジュールクラスの配列を受け取ります。今回の場合はMyModule
のクラスを要素にもつ配列を渡してやればよいです。
次に、依存性注入用の関数を定義します。fun inject(任意の引数名: DIするクラス型)
で定義します。今回の場合、Character
クラスをインジェクトするので、
@Component(modules = [MyModule::class])
interface MyComponent {
fun inject(character: Character)
}
となります。これはあくまでコンポーネントを定義しただけです。実際の依存性注入は手動でやる必要があります。もうひとふんばり!
このコンポーネントを定義すると、DaggerMyComponent
というのができてるはずです。Daggerコンポーネント名
の命名規則に従います。こちらを用いてコンポーネントをビルドします。
val component = DaggerMyComponent.builder()
.myModules(MyModules())
.build()
こちらのコンポーネントには先程定義したinject(character: Character)
メソッドが実装されていますので、そちらを呼び出してインジェクトします。
val character = Character()
component.inject(character)
これでArmer
クラスとWeapon
クラスのインスタンスが注入されました。これ、実際にやっていることは
val character = Character()
character.armer = Armer()
character.weapon = Weapon()
と変わりません。component
が外側に出ているのが気に食わない?そうですね。init
の中に突っ込んでしまえばいいのではないでしょうか。
open class Character {
@Inject lateinit var armer: Armer
@Inject lateinit var weapon: Weapon
private val component = DaggerMyComponent.builder()
.myModules(MyModules())
.build()
init {
this.component.inject(this)
}
}
こうすれば、
val character = Character()
で自動でインジェクトされますね。
DI、完全に理解しましたか
結構強引に進めちゃった気がしていますが、どうでしょう。前は単語を聞いただけで頭が痛くなっていたDependency Injection、完全に理解できたのではないでしょうか。
完全に理解したあとは、挫折が待っているかもしれません。Daggerの使い方がようわからん!内部挙動がようわからん!色々苦労するかもしれません。でも、今回の記事で多少なりDIに対する恐怖心が薄れたのであれば、非常に嬉しく思います。
ではでは!よいプログラミングライフを。DI知りたて、Dagger知りたてほやほやの未熟者ですので、何かしらとんでもない誤解をしているかもしれません。もし何かしら間違ってるとかおかしいとかありましたら、ぜひ容赦なく首突っ込んでください。問題なさそうだったら、いいね or コメントいただけると非常に安心します!
参考文献
- Kotlinで DI (Dependency Injection)~ Dagger 編 - Qiita ( https://qiita.com/sudachi808/items/a05237e1294639ea41dd )
- Android での依存関係インジェクション | Android デベロッパー | Android Developers ( https://developer.android.com/training/dependency-injection?hl=ja )
- Dagger の基本 | Android デベロッパー | Android Developers ( https://developer.android.com/training/dependency-injection/dagger-basics?hl=ja )