LoginSignup
1
3

More than 1 year has passed since last update.

【Dagger】全くわからないDependency Injectionを完全に理解するための記事

Last updated at Posted at 2022-09-24

皆さん、こんにちは。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にはArmerWeaponを装備できます。CharacterArmerWeaponを持っていると考えたら、以下のような設計が考えられます。内部にメンバとして定義してしまう、というものです。

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()

まあ確かに、こうすれば装備変更は可能だけど、、。後から代入するの忘れたときとか、armerweaponが直接置き換えられるようになってて危険じゃない?
その上、キャラクターを作るときに必ず防具と武器が必要っていうんなら、作るときに渡してあげるのが自然じゃないかな。じゃあ、こう?

open class Character (val armer: Armer, val weapon: Weapon) { }

いいですね。これなら色んな武器や防具を渡して、様々なキャラクターを作れて便利そうです。

・・・ はい。これが、Dependency Injectionです。・・・ええ?「なるほど、わからん」ですか。次節でもうちょっと詳しく説明します。

一体どこがDIなの?

凄く当たり前にやることなので、気付かない方も多いかもしれませんが、DIはとても身近な存在なのです。
先程の例について、詳しく説明します。

キャラクターを作るときに必ず防具と武器が必要っていうんなら、作るときに渡してあげるのが自然じゃないかな。

と、わざと太字にしました。これ、難しく言うと次のように表現できます。

Characterクラスのインスタンス化にArmerWeaponが必要なら、Characterクラスのコンストラクタに渡してあげるのが自然じゃないかな。

もっと行きましょう。

CharacterクラスがArmerWeapon依存している なら、コンストラクタで 注入してあげる のが自然じゃないかな。

おや、「依存している」という表現が出てきました。また、「注入してあげる」という言葉も出てきました。
依存」と「注入」・・・? 依存性注入・・・?? これ、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 {}

では、ArmerWeaponを供給(@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 コメントいただけると非常に安心します!

参考文献

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3