こんにちは。新人エンジニアの岩本です。
今回は本や業務でよく見かける「DI(Dependency Injection)」について、自分なりの調べたことをまとめたいと思います。
DIは業務では当たり前のように使われるので、理解しておいて損はないと思います。またDIを用いることで、より柔軟なコードを書けるようになります。
実装環境
- 言語…Swift 5.8.1
- OS… macOS Ventura 13.1
- アプリ… Xcode 14.3.1
DI(Dependency Injection)とは?
そもそもDIは日本語で言うと「依存性の注入」です。名前からも難しそうな雰囲気が漂ってますが、本質はシンプルです。
DIとは「依存関係を外部から指定する」ことです。
class ViewModel {
private let sampleAPI: SampleAPIProtocol
init(sampleAPI: SampleAPIProtocol) {
self.sampleAPI = sampleAPI
}
}
let viewModel = ViewModel(sampleAPI: SampleAPI()) // 外部から依存関係を指定
ポイントはProtocolを使用することです。
API通信などは開発時はテスト環境で、開発が進んできたら本番環境に切り替えます。
この時Protocolを使用していると、依存関係を注入する際に、貰う側のコード(この例だとViewModel)は変更する必要がなくなります。
コードにすると、たったこれだけのシンプルな概念です。シンプルな概念ですが、使用することによって様々なメリットがあります。
DIを「ピザ屋」に例える
DIを「🍕ピザ屋」に例えてみましょう。
通常ピザ屋で注文するときは、ピザ本体とトッピングを指定することができます。DIはこのピザとトッピングの指定にあたります。
もしピザ屋さんがDIに即していない場合、作られるピザやトッピングは外部から指定できずに固定になります。もし違うピザが食べたい場合は、ピザ屋を変更しなければいけません。
しかし現実にはピザ屋はDIができる、外部からピザとトッピングを指定することができます。
では話をプログラミングに戻して実際のメリットと実装例を紹介します。
DIを使うメリット
DIを使うことの大きなメリットは、クラス間を疎結合にできることです。
このメリットを感じるために、まずはDIを使わない例を考えてみましょう。
class TestAPI {
// ...
}
class ViewModel {
private let api: TestAPI // Test用のAPI
init(api: TestAPI) {
self.api = api
}
}
let viewModel = ViewModel(api: TestAPI())
ここではViewModelはTest用のAPIをプロパティとして持っています。これで問題なく動作はします。
しかし、ここでAPIを本番用のものに切り替えることになりました。コードを書き換えてみましょう。
class ProdAPI {
// ...
}
class ViewModel {
private let api: ProdAPI // 本番用のAPI
init(api: ProdAPI) {
self.api = api
}
}
let viewModel = ViewModel(api: ProdAPI())
APIを本番用に切り替えたいだけなのに、ViewModelを変更する必要がありました。もしかしたらこの変更によりバグが発生してしまうかもしれません。
ではDIを使ってコードを書き直してみましょう。DIのポイントはProtocolを使うことです。Protocolを使うことで、具体的な型やクラスにとらわれなくなります。
protocol APIProtocol {
// ...
}
class TestAPI: APIProtocol {
// ...
}
class ProdAPI: APIProtocol {
// ...
}
APIProtocolを定義し、API通信を行うクラスはこれを準拠するようにします。こうすることでViewModelをこんな風に書き直すことができます。
class ViewModel {
private let api: APIProtocol // APIProtocolを指定
init(api: APIProtocol) {
self.api = api
}
}
let testViewModel = ViewModel(api: TestAPI())
let prodViewModel = ViewModel(api: ProdAPI())
こうして外部から依存関係を指定することで、ViewModelのコードを変更せずとも、テスト用と本番用のAPIの切り替えを行えるようになりました。
具体的な型を指定するのではなく、抽象的な型を指定することでクラス間を疎結合にすることができます。疎結合になれば保守がしやすくテストもしやすくなります。
大きいプロジェクトになる程、保守のしやすさは重要になってくるので実際の業務ではよく使われるのです。
似たような概念で「依存関係逆転の原則」というものもあります。ぜひ一緒にチェックしてみてください。
ここまでDIのメリットと簡単な実装例を紹介したので、ここから先は少しステップアップして、Contanerを使ったDIの実装を紹介します。
【ステップアップ】Containerを使ってDIを実装
DIが便利だと言っても、複数の依存関係を指定しようとすると少々複雑になってしまいます。
そこでContainerを作って、依存関係の注入の役割を一点に集中させる方法を紹介します。
protocol DIContainerProtocol {
func register<Service>(type: Service.Type, component: Any)
func resolve<Service>(type: Service.Type) -> Service?
}
class DIContainer: DIContainerProtocol {
static let shared = AwesomeDIContainer()
private init() {}
var services: [String: Any] = [:]
// 型とインスタンスを結びつける
func register<Service>(type: Service.Type, component: Any) {
services["\\(type)"] = component
}
// 指定した型のインスタンスを返す
func resolve<Service>(type: Service.Type) -> Service? {
return services["\\(type)"] as? Service
}
}
少し複雑なコードですが、使い方はシンプルです。
let container = DIContainer.shared
container.register(type: APIProtocol.self, component: SampleAPI()) // 型とインスタンスを紐付け
let viewModel = ViewModel(api: container.resolve(type: APIProtocol.self)!)
このContainerを使うことで、依存関係の処理を1つにまとめることができます。
また自分で作らなくても同じことを行うライブラリ等もあるので、ご自身の手で試してみてください。
まとめ
今回は「DI(Dependency Injection)」について解説しました。
名前の割にはとてもシンプルな概念ということがおわかりいただけたと思います。でもこれを意識するだけでもより柔軟で変更に強いコードが書けるようになると思います。
ぜひご自身の手で色々と試してみてください。
ここまでのご閲覧ありがとうございました!
コメント