
みなさまこんにちは〜!
メモリアインクのすだです。
Kotlinで開発をしていると、よく依存性注入という言葉が出てきます。
今回は、この依存性注入について基本からその必要性まで細かく解説していきます。
この記事を読んでわかること…
・DI(依存性注入)とは?
・DI(依存性注入)の必要性
・DI(依存性注入)の実装方法
環境
- Kotlin (ver 1.9.0)
- Android Studio (Giraffe | 2022.3.1 Patch 3)
DI(依存性注入)とは?
DI(Dependency Injection) = 依存性注入とは、
「あるクラスが必要とする別のクラス(依存)を、自分で作らず、外部から渡してもらう仕組み」のことです。
fun main() {
val service = HelloService() // ← 依存を自分で用意して
val greeter = Greeter(service) // ← 外から注入して渡す
greeter.greet() // → "こんにちは!" と表示
}
上記では3行目が依存性注入の例です。
ここで、Greeter
が自分で HelloService
を作っていないことがポイントです。
➡ 必要なもの(依存)を外から注入してもらう というのが「DI(依存性注入)」です。
DIを実装するには、手動または専用ライブラリを使うことになります。
手動で実装する方法
手動の場合は、クラスのコンストラクタで引数として依存を受け取る というやり方です。
// 依存クラス
class ApiClient {
fun fetchUser(): String = "Taro"
}
// 依存を使うクラス
class UserRepository(private val apiClient: ApiClient) {
fun getUser(): String = apiClient.fetchUser()
}
// 呼び出し処理
fun main() {
val apiClient = ApiClient()
val userRepository = UserRepository(apiClient)
println(userRepository.getUser()) // → Taro
}
このようにUserRepositoryのコンストラクタで引数としてApiClient(依存)を受け取ることで実装できます。
手動DIは
・シンプルなコンストラクタベースで依存関係を注入できるため、初心者でも取り組みやすい
・依存関係の流れがコード上ではっきり見えるため、「どのクラスがどの依存を使っているか」が把握しやすい
というメリットがあります。
しかし、反対に
・コンポーネント数が増えるにつれて、依存関係の組み立てや初期化処理が複雑になり、可読性や保守性が下がる
・ActivityやViewModelなどライフサイクルに応じた依存性の注入が必要な場面が多く、手動で対応するのが難しくなるケースがある
というデメリットもあります。
これらのデメリットを考慮すべく、ライブラリを使用することが多いです。
専用ライブラリ
DIを実装できるライブラリはいくつかありますが、
Hiltを使ったDIをお勧めします。(公式推奨)
HiltのDIについては別の記事で詳細をご紹介しておりますので、ぜひ併せてご覧ください。
なぜ依存性注入(DI)が必要なのか?
DIを使うと「依存関係の整理・再利用・テスト」が圧倒的に楽になります。
ここで、DIしない場合とする場合を比較して確認してみましょう。
DIしない場合(依存を自分で作る設計)
class ApiClient {
fun fetchData(): String = "データ取得"
}
class UserRepository {
private val apiClient = ApiClient() // 自分で依存をnewしている
fun getUser(): String = apiClient.fetchData()
}
class UserManager {
private val repository = UserRepository() // また自分で作る
fun process() {
println(repository.getUser())
}
}
UserRepository は、自身の中で ApiClient のインスタンスを生成して、その処理を利用しています。
さらに UserManager は、「自ら ApiClient を生成している UserRepository」のインスタンスを、自身で生成して使用しています。
このように、UserManager → UserRepository → ApiClient
と依存関係が連鎖的にネストして構築されている状態では、たとえば ApiClient に変更が入った場合、UserRepository の修正が必要になり、さらに UserManager にも影響が及ぶ可能性があります。
また、同じ依存(ApiClient など)を複数の場所で使いたい場合、都度 new してインスタンスを生成する必要があり、重複・管理の煩雑化・拡張性の低下といった問題を引き起こします。
このような設計は、特に中〜大規模な開発では保守性・テスト性・再利用性に大きな課題を生みます。
これを解決する方法が、DI(依存性注入)です。↓
DIする例(依存を外から注入)
class ApiClient {
fun fetchData(): String = "データ取得"
}
class UserRepository(private val apiClient: ApiClient) {
fun getUser(): String = apiClient.fetchData()
}
class UserManager(private val userRepository: UserRepository) {
fun process() {
println(userRepository.getUser())
}
}
UserRepositoryのコンストラクタでApiClientを受け取ることによって、ApiClientのインスタンスが注入されます。
同様に、UserManagerのコンストラクタでUserRepositoryを受け取ることによって、UserRepositoryのインスタンスが注入されます。
fun main() {
// 呼び出し側で明示的に依存関係を組み立てる
val apiClient = ApiClient()
val repository = UserRepository(apiClient)
val manager = UserManager(repository)
manager.process()
}
このように、必要な依存をすべて呼び出し側で組み立てて渡す設計(依存性注入)にすることで、
クラス同士が互いに直接生成し合うことがなくなり、依存関係を緩やかに保つことができます。
たとえば ApiClient
の内部処理に修正が加わったとしても、
他のクラスがそれを自分で生成していない構造であれば、修正は呼び出し元の一箇所だけで済む設計になります。
まとめ
おつかれさまでした。いかがでしたでしょうか!
DIは設計の透明性・保守性・テストのしやすさを飛躍的に高めてくれます。
まずは「手動で注入する」ことから始め、必要に応じてHiltの使用 とステップアップしていくのがおすすめです。



技術者としてのキャリアパスを次のレベルへと進めたい皆様、
<未経験からIT・Webエンジニアを目指すなら【ユニゾンキャリア】>
自分の市場価値をさらに向上させてみませんか?
それではまた次回の記事でお会いしましょう!
コメント