依存性の注入(DI)は、オブジェクトが依存関係を自ら作成するのではなく、外部から受け取る手法です。DI は IoC(制御の逆転)原則の実装であり、Dagger、Hilt、Swinject の基盤となっています。依存性の注入はコードの結合度を減らし、テストを簡素化し、アーキテクチャを柔軟にします。Android では Google の Dagger Hilt が DI の標準であり、iOS では Swinject または手動注入が使われます。詳細は Android DI ガイドをご覧ください。
重要なポイント
依存性の注入は、オブジェクトがコンストラクタ、セッター、またはインターフェースを通じて依存関係(サービス、リポジトリ、設定)を受け取り、自ら new で作成しない手法です。DI の目的はクラス間の結合度を減らすことです。クラスが自ら依存関係を作成する場合、特定の実装に強く結びつき、テストと変更が困難になります。DI では、クラスは抽象(プロトコル/インターフェース)に対して動作し、具象実装は外部から提供されます。
3 つの注入方法 — コンストラクタ注入(init/constructor 経由)、セッター注入(プロパティ/setter 経由)、インターフェース注入(インターフェースメソッド経由)。コンストラクタ注入が推奨される方法です。依存関係がシグネチャで明確に可視化され、オブジェクトは常に有効な状態で作成されます。セッター注入はデフォルト値を持つオプショナルな依存関係に使用されます。インターフェース注入は稀で、主に DI コンテナで使用されます。
| DI の種類 | 方法 | 使用する場面 | 例 |
|---|---|---|---|
| コンストラクタ | イニシャライザのパラメータ | 必須の依存関係 | init(service: ServiceProtocol) |
| プロパティ | クラスのプロパティ | オプショナルな依存関係 | var service: ServiceProtocol? |
| メソッド | メソッドのパラメータ | 一時的な依存関係 | func doWork(with service: Service) |
DI コンテナ — 依存関係の作成とライフサイクルを管理するライブラリです。コンテナは型の登録(各抽象型を具象実装にマッピング)と、解決済みの依存関係を持つオブジェクトを作成するファクトリを保持します。Android では Dagger/Hilt、iOS では Swinject、Needle、Dip が使われます。コンテナはスコープを管理できます:シングルトン(アプリケーション全体で1つのインスタンス)、フィーチャースコープ(画面ごと)、またはリクエストごとに新しいオブジェクト。
Dagger Hilt は Google の Dagger をラップしたもので、Android 向け標準 DI ライブラリです。Hilt は Dagger を簡素化します:手動でのコンポーネント作成を不要にし、@HiltAndroidApp、@AndroidEntryPoint、@Module を追加します。Hilt は Android のライフサイクルと統合され、ViewModel、Activity、Fragment、Service、BroadcastReceiver がアノテーションを通じて依存関係を受け取れます。コード生成はコンパイル時に行われ、Dagger がコンポーネントの実装を生成するため、ランタイムオーバーヘッドはゼロです。
// アプリケーションクラス
@HiltAndroidApp
class MyApp : Application()
// モジュール — 依存関係の作成方法を定義
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build()
@Provides
@Singleton
fun provideApiService(client: OkHttpClient): ApiService {
return Retrofit.Builder()
.baseUrl("https://api.example.com")
.client(client)
.build()
.create(ApiService::class.java)
}
}
// ViewModel がコンストラクタを介して依存関係を受け取る
@HiltViewModel
class MainViewModel @Inject constructor(
private val apiService: ApiService
) : ViewModel() {
private val _state = MutableStateFlow(MainState.Loading)
val state: StateFlow<MainState> = _state.asStateFlow()
fun loadData() {
viewModelScope.launch {
_state.value = MainState.Success(apiService.getData())
}
}
}
// Activity — @AndroidEntryPoint が DI を有効にする
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
}
Dagger のコンポーネントとスコープ — @Singleton(アプリケーション全体)、@ActivityScoped(Activity ごと)、@FragmentScoped(Fragment ごと)、@ViewModelScoped(ViewModel ごと)。スコープの選択がオブジェクトのライフタイムを決定します。@Singleton — プロセス全体で1つのインスタンス、OkHttpClient やデータベースに適しています。@ActivityScoped — Activity が存続する間オブジェクトが生存し、画面レベルの依存関係に使用します。@ViewModelScoped — Hilt 2.45+ の新機能で、ViewModel が存続する間オブジェクトが生存し、コルーチンスコープに便利です。
Swinject は iOS 向けの人気のオープンソース DI フレームワークです。Swinject は Container、Assemblies、およびさまざまなスコープを提供します。Dagger とは異なり、Swinject はランタイムで動作し、依存関係はコード生成なしで動的に解決されます。これにより Swinject のセットアップは簡単ですが、デバッグは困難です:未解決の依存関係エラーはランタイムでのみ現れます。Swinject はコンストラクタ注入、プロパティ注入、メソッド注入をサポートしています。
import Swinject
// Assembly — 登録のグループ
class NetworkAssembly: Assembly {
func assemble(container: Container) {
container.register(NetworkServiceProtocol.self) { _ in
NetworkService()
}.inObjectScope(.container) // singleton
container.register(UserRepositoryProtocol.self) { r in
UserRepository(
networkService: r.resolve(NetworkServiceProtocol.self)!
)
}
}
}
// ViewModel がコンストラクタ注入を介して
class ProfileViewModel: ObservableObject {
private let repository: UserRepositoryProtocol
init(repository: UserRepositoryProtocol) {
self.repository = repository
}
@Published var user: User?
func loadUser() {
repository.fetchUser { [weak self] user in
self?.user = user
}
}
}
// AppDelegate または App での DI 設定
let assembler = Assembler([NetworkAssembly(), ServiceAssembly()])
let viewModel = assembler.resolver.resolve(ProfileViewModel.self)!
// UIKit ViewController のプロパティ注入
container.register(UserViewController.self) { r in
let vc = UserViewController()
vc.viewModel = r.resolve(ProfileViewModel.self)
return vc
}
Swinject のスコープ — .transient(毎回新しいオブジェクト)、.container(コンテナごとのシングルトン)、.graph(デフォルト — 1つの依存関係グラフ内でオブジェクトが共有されます)。iOS アプリケーションでは .container と .transient で十分です。Swinject は Assembler もサポートしており、モジュラーアーキテクチャのための Assembly のグループ化が可能です。テストでは、Assembly を MockAssembly に置き換えることで、プロダクションコードを変更せずに依存関係の置換が可能です。
DI vs Service Locator — どちらのパターンも依存関係管理の問題を解決しますが、方法が異なります。DI はオブジェクトに依存関係を注入します。Service Locator はグローバルレジストリを提供し、オブジェクトが自ら依存関係を要求します。DI はコンストラクタ(またはセッター)を通じて依存関係を明示的に宣言します。Service Locator は依存関係を隠蔽し、メソッド内部で要求されるため、シグネチャの情報量が減ります。DI はテストが容易で、コンストラクタにモックを渡すだけです。Service Locator はテストごとにグローバルレジストリの設定が必要です。
| 特性 | 依存性の注入 | Service Locator | 手動注入 |
|---|---|---|---|
| 依存関係の明示性 | コンストラクタ内 | メソッド本体に隠蔽 | 明示的 |
| テスト | コンストラクタにモック | Locator の設定 | コンストラクタにモック |
| セットアップの複雑さ | DI コンテナが必要 | グローバルレジストリ | 手動作成 |
| ランタイムオーバーヘッド | Dagger — コンパイル時 | ランタイム検索 | なし |
DI vs 手動注入 — DI コンテナがない場合、依存関係はファクトリや AppDelegate で手動で作成されます。5〜10 クラスであれば、手動注入の方がシンプルで、Dagger や Swinject を学ぶ必要はありません。50 クラス以上になると、手動注入は問題になります:5〜6 個のパラメータを持つコンストラクタ、複雑な作成順序、コードの重複。DI コンテナはこれらのプロセスを自動化し、明確なライフサイクル管理を提供します。コンテナなしの手動注入は、小規模プロジェクトやプロトタイプに適しています。
コンストラクタ注入 — 標準。必須の依存関係には常にコンストラクタ注入を使用してください。これにより依存関係が明示的になり、オブジェクトは常に動作可能な状態になります。セッター注入はオプショナルな依存関係のみに使用します(例:delegate や listener)。インターフェース注入は、独自の DI ライブラリを書いている場合を除き、使用しないでください。コンストラクタ注入は、オブジェクトが有効な状態で作成されることを保証する唯一の方法です。
1 クラス 1 責任。クラスのコンストラクタが 5 つ以上のパラメータを必要とする場合、そのクラスは単一責任原則に違反している可能性があります。クラスをより少ない依存関係を持つ複数のクラスに分割してください。兆候:6 つの異なるサービスを持つ ServiceManager クラスを作成している場合、それは God Object アンチパターンです。ビジネスロジックを Use Cases(Interactors)に抽出し、それぞれ 1〜2 つの依存関係を持たせてください。
// ❌ 悪い例:6つの依存関係 — God Object
class ProfileViewModel @Inject constructor(
private val api: ApiService,
private val db: Database,
private val analytics: Analytics,
private val prefs: Preferences,
private val location: LocationProvider,
private val notification: NotificationManager
)
// ✅ 良い例:1〜2の依存関係を持つ Use Cases
class ProfileViewModel @Inject constructor(
private val loadProfileUseCase: LoadProfileUseCase,
private val trackAnalyticsUseCase: TrackAnalyticsUseCase
)
スコープとライフサイクル — 各依存関係に適切なスコープを選択してください。シングルトン:OkHttpClient、データベース、SharedPreferences。フィーチャースコープ:リポジトリ、Use Cases(ステートレスの場合)。Transient:Value Objects、DateFormatter、パーサー。スコーピングの誤りはよくある問題で、画面状態を保持するシングルトンはリークを引き起こします。Android では Hilt の @ActivityScoped がこれを解決し、Swinject では .container を慎重に使用してください。
よくある質問
new はクラス間に強い結合を生み出します — コードを変更せずに実装を交換することはできません。テストが難しくなります:実際のサービスの代わりにモックを注入できません。SRP に違反します:クラスがビジネスロジックと依存関係作成の両方に責任を持ちます。DI は外部から依存関係を注入し、抽象に対して動作することでこれらの問題を解決します。
Dagger Hilt は Google の標準で、コード生成によるコンパイル時 DI、優れたパフォーマンス、Jetpack との統合を備えています。Koin はランタイム DI で、セットアップは簡単ですが、低速でランタイムエラーが発生します。プロダクションプロジェクトには Hilt を選んでください。Koin はプロトタイプや小規模アプリケーションに適しています。
いいえ。iOS では Swinject(ランタイム、人気)、Needle(Uber 製のコンパイル時)、Dip(軽量)、Weaver(Sourcery ベース)などが利用可能です。Apple は組み込みの DI コンテナを提供していませんが、init による手動注入が標準的なプラクティスです。SwiftUI では、外部ライブラリなしで Environment や @StateObject による手動 DI で十分なことがよくあります。
はい。コンストラクタによる手動注入はフレームワークなしの DI です。Service Locator はフレームワークなしの代替手段です。ファクトリや Factory Method も DI の一形態です。フレームワーク(Dagger、Swinject)はルーチン的な登録と依存関係の解決を自動化しますが、10〜20 クラスであれば手動 DI で十分です。
DI は制御の逆転(IoC)原則を実装する手法(テンプレート)です。GoF パターンとは異なり、DI に厳格な 3〜4 クラスの構造はありません。DI は依存関係を整理する方法であり、デザインパターンではありません。DI コンテナ(Dagger、Swinject)はこの手法を自動化するフレームワークです。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。