はじめに
Swiftにおける型パラメータインジェクションを紹介します。
環境
- OS:macOS Monterey 12.3.1
- Xcode:13.3.1 (13E500a)
- Swift:5.6
「型パラメータインジェクション」とは?
「型パラメータインジェクション(Type Parameter Injection)」はDIの手法のひとつで、ジェネリクスの型引数を使って依存性を注入することです。
protocol FooProtocol {
static func foo()
}
final class Bar<F: FooProtocol> { // 型引数で依存性を注入する
func bar() {
F.foo()
}
}
この手法はTwitterで教えていただきました。
本記事ではkoherさんに合わせて「型パラメータインジェクション」と呼んでいます。
正式名称があるかどうかはわかりません。
他のインジェクションとの比較
「バスタードインジェクション(Bastard Injection)」 1 とコンストラクタインジェクション、型パラメータインジェクションを比較します。
バスタードインジェクション
まずはバスタードインジェクションを紹介します。
Foo
をシングルトンにして、Bar
のイニシャライザのデフォルト引数に指定します。
protocol FooProtocol {
func foo()
}
struct Foo {
static let shared = Foo()
private init() {} // シングルトンにするため、イニシャライザを外部に公開しない
}
extension Foo: FooProtocol {
func foo() {
print("Foo")
}
}
final class Bar {
private let foo: any FooProtocol
init(foo: any FooProtocol = Foo.shared) { // イニシャライザのデフォルト引数で依存性を注入する
self.foo = foo
}
func bar() {
foo.foo()
}
}
Bar().bar()
を実行すると、 Foo
と出力されます。
Bar().bar() // "Foo"
Foo
をモックに差し替えるのが容易なため、 Bar
はテスタブルであり、大きな問題はありません。
しかし以下のデメリットがあります。
- 存在型(
any
)を使っている 2 - 状態に依存しないことが保証されていない
-
Foo
のインスタンスを生成しているため
-
- シングルトンにするためのボイラーテンプレートが存在する
コンストラクタインジェクション
「存在型を使っている」デメリットを解消するため、 FooProtocol
プロトコルを Bar
の型引数に指定します。
- final class Bar {
- private let foo: any FooProtocol
+ final class Bar<F: FooProtocol> {
+ private let foo: F
- init(foo: any FooProtocol = Foo.shared) { // イニシャライザのデフォルト引数で依存性を注入する
+ private init(foo: F) {
self.foo = foo
}
+
+ convenience init() where F == Foo {
+ self.init(foo: .shared)
+ }
func bar() {
foo.foo()
}
}
これで存在型がなくなり、デメリットがひとつ減りました。
存在型(any
)を使っている- 状態に依存しないことが保証されていない
-
Foo
のインスタンスを生成しているため
-
- シングルトンにするためのボイラーテンプレートが存在する
しかしボイラーテンプレートが増えてしまいました。
コンビニエンスイニシャライザを使ったデフォルト値の指定方法の詳細は、以下の記事をご参照ください。
型パラメータインジェクション
残り2つのデメリットを一気に解消するのが、型パラメータインジェクションです。
Foo
の foo()
メソッドは文字列を標準出力しているだけなので、静的にできます。
静的にできるかプロトコルからはわからないこともありますが、今回はわかるものとします。
protocol FooProtocol {
- func foo()
+ static func foo()
}
Foo
のインスタンスを生成する必要がなくなったので、構造体から列挙型に変更します。
- struct Foo {
+ enum Foo {
- static let shared = Foo()
-
- private init() {} // シングルトンにするため、イニシャライザを外部に公開しない
}
extension Foo: FooProtocol {
- func foo() {
+ static func foo() {
print("Foo")
}
}
Foo
のインスタンスが生成できなくなり、状態に依存しないことが保証されます。
Bar
から不要になった定数やイニシャライザを削除します。
final class Bar<F: FooProtocol> {
- private let foo: F
-
- private init(foo: F) {
- self.foo = foo
- }
-
- convenience init() where F == Foo {
- self.init(foo: .shared)
- }
-
func bar() {
- foo.foo()
+ F.foo()
}
}
Foo
を外部から注入するためのボイラープレートをすべて削除できました。
Bar<Foo>().bar()
を実行すると、 Foo
と出力されます。
Bar<Foo>().bar() // "Foo"
これで型パラメータインジェクションの実装は完成です。
型引数にはデフォルトの型を指定できないため、呼び出し時に型を明示的に指定する必要があります。
しかし呼び出す側( Bar
)が呼び出される側( Foo
)に依存しなくなるので、一般的にはメリットといえます。
モックに差し替えるのも簡単です。
enum FooMock {
}
extension FooMock: FooProtocol {
static func foo() {
print("FooMock")
}
}
Bar<FooMock>().bar() // "FooMock"
これで最初に挙げたデメリットがすべて解消されました。
存在型(any
)を使っている-
状態に依存しないことが保証されていないFoo
のインスタンスを生成しているため
シングルトンにするためのボイラーテンプレートが存在する
おわりに
型パラメータインジェクションの紹介でした。
コードがスッキリするため、インスタンスを生成する必要がないときは、積極的に使ってもいい手法だと思います。
参考リンク
- https://twitter.com/koher/status/1514138781049241604
- https://twitter.com/koher/status/1501426736507985922
-
イニシャライザのデフォルト引数でインスタンスを注入すること。 ↩
-
基本的に存在型は使わないのが望ましい。https://heart-of-swift.github.io/protocol-oriented-programming/#:~:text=プロトコルを「型として」ではなく「制約として」使用することを優先する ↩