30
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

型パラメータインジェクション - Type Parameter Injection(Swift)

Last updated at Posted at 2022-05-01

はじめに

Swiftにおける型パラメータインジェクションを紹介します。

環境

  • OS:macOS Monterey 12.3.1
  • Xcode:13.3.1 (13E500a)
  • Swift:5.6

「型パラメータインジェクション」とは?

「型パラメータインジェクション(Type Parameter Injection)」はDIの手法のひとつで、ジェネリクスの型引数を使って依存性を注入することです。

FooProtocol.swift
protocol FooProtocol {
    static func foo()
}
Bar.swift(型パラメータインジェクション)
final class Bar<F: FooProtocol> { // 型引数で依存性を注入する
    func bar() {
        F.foo()
    }
}

この手法はTwitterで教えていただきました。
本記事ではkoherさんに合わせて「型パラメータインジェクション」と呼んでいます。

正式名称があるかどうかはわかりません。

他のインジェクションとの比較

「バスタードインジェクション(Bastard Injection)」 1 とコンストラクタインジェクション、型パラメータインジェクションを比較します。

バスタードインジェクション

まずはバスタードインジェクションを紹介します。

Foo をシングルトンにして、Bar のイニシャライザのデフォルト引数に指定します。

FooProtocol.swift
protocol FooProtocol {
    func foo()
}
Foo.swift
struct Foo {
    static let shared = Foo()

    private init() {} // シングルトンにするため、イニシャライザを外部に公開しない
}

extension Foo: FooProtocol {
    func foo() {
        print("Foo")
    }
}
Bar.swift(バスタードインジェクション)
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 の型引数に指定します。

Bar.swift(コンストラクタインジェクション)
- 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つのデメリットを一気に解消するのが、型パラメータインジェクションです。

Foofoo() メソッドは文字列を標準出力しているだけなので、静的にできます。
静的にできるかプロトコルからはわからないこともありますが、今回はわかるものとします。

FooProtocol.swift
protocol FooProtocol {
-   func foo()
+   static func foo()
}

Foo のインスタンスを生成する必要がなくなったので、構造体から列挙型に変更します。

Foo.swift
- struct Foo {
+ enum Foo {
-   static let shared = Foo()
- 
-   private init() {} // シングルトンにするため、イニシャライザを外部に公開しない
}

extension Foo: FooProtocol {
-   func foo() {
+   static func foo() {
        print("Foo")
    }
}

Foo のインスタンスが生成できなくなり、状態に依存しないことが保証されます。

Bar から不要になった定数やイニシャライザを削除します。

Bar.swift(型パラメータインジェクション)
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 )に依存しなくなるので、一般的にはメリットといえます。

モックに差し替えるのも簡単です。

FooMock.swift
enum FooMock {
}

extension FooMock: FooProtocol {
    static func foo() {
        print("FooMock")
    }
}
Bar<FooMock>().bar() // "FooMock"

これで最初に挙げたデメリットがすべて解消されました。

  • 存在型( any )を使っている
  • 状態に依存しないことが保証されていない
    • Foo のインスタンスを生成しているため
  • シングルトンにするためのボイラーテンプレートが存在する

おわりに

型パラメータインジェクションの紹介でした。
コードがスッキリするため、インスタンスを生成する必要がないときは、積極的に使ってもいい手法だと思います。

参考リンク

  1. イニシャライザのデフォルト引数でインスタンスを注入すること。

  2. 基本的に存在型は使わないのが望ましい。https://heart-of-swift.github.io/protocol-oriented-programming/#:~:text=プロトコルを「型として」ではなく「制約として」使用することを優先する

30
15
1

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
30
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?