I. はじめに
APIをViewControllerなどに持たせる場合、直接APIを持たせるのではなく、modelを取得するインターフェースを持つprotocolを作成し、それに準拠した実態としてのAPIをDIすると言うやり方で実装する人が多いのではないだろうか。
protocolをgenericにしてAPIのレスポンスの型を差し込み使いまわしたいところだが、protocolをgenericにしてしまうと、それを変数として宣言できなくなってしまうため、毎回個別にprotocolを定義しなければいけない。
今回はこの問題をType Erasureと言うテクニックを使い、genericだが変数として保持可能なprotocolを作成することで解決したと思う。
II. よくある実装例
protocolを作成
ユーザーを取得するインターフェースをprotocolとして作成
protocol UsersRequestable {
func fetch(completion: ((Result<[User], Error>) -> Void))
}
実態のAPIクラスを作成
本番用のものとStubの2つのAPIを先ほどのUsersRequestableへ準拠させて作成。
/// 実際にサーバーへ繋げるAPI
class UsersRequest: UsersRequestable {
func fetch(completion: ((Result<[User], Error>) -> Void)) {
// サーバーからデータを取ってくる
}
}
/// 開発や自動化テスト用のStubを返すAPI
class StubUsersRequest: UsersRequestable {
func fetch(completion: ((Result<[User], Error>) -> Void)) {
// Stubの情報を返す
}
}
ViewControllerへDI
先ほど作成したUsersRequestおよびStubUsersRequestは、ViewControllerへ状況に応じてDIされる。
UsersRequestableを変数として保持
実際に中に何が入っているかは感知しない。
class MyViewController: UIViewController {
var usersRequest: UsersRequestable?
}
状況に応じたAPIを差し込む
// 本番APIを差し込む例
let myViewController = MyViewController()
myViewController.userRequest = UsersRequest() // 本番APIを差し込む
// Stub APIを差し込む例
let myViewController = MyViewController()
myViewController.userRequest = StubUsersRequest() // Stubを差し込む
モデル毎にprotocolの作成が必要
この例の実装ではUserに対してはUsersRequestable, 他のモデルがある場合はそのモデルに対してのRequestable protocolを作らなければいけない。すなわち全てのモデルに対してRequestable protocolを作成しなければならないことになる。これは完全なDRY違反である。
III. generic protocolは変数保持が不可
モデル毎にRequestableを作らなければいけないという問題に対処するためにgenericなprotocolを作成すると言う方法が考えられる。しかしgenericなprotocolはいくつかの問題があり、Requestableとして使えない。
単純なGeneric protocolは作れない
protocol名という宣言のgeneric protocolはそのそも作成できない。Associated Typeを使えと言われてしまう。
protocol AnyRequestable<T> {
func fetch(completion: ((Result<[T], Error>) -> Void))
}
// コンパイルエラー: Protocols do not allow generic parameters; use associated types instead
Associated Typeのprotocolは変数保持は不可
associated typeを使いgenericなprotocolは以下のように作成することができる。このケースではAnyRequestableのprotocolはコンパイルを通る。
// これはコンパイルを通る
protocol AnyRequestable {
associatedtype T
func fetch(completion: ((Result<[T], Error>) -> Void))
}
しかしAnyRequestableを変数として保持させようとするとコンパイルエラーになってしまう。
class MyViewController: UIViewController {
var usersRequest: AnyRequestable?
// コンパイルエラー: Protocol 'AnyRequestable' can only be used as a generic constraint because it has Self or associated type requirements
}
IV. TypeErasureで解決
Type Erasureとはgenericなprotocolを作成してそれを変数として保持することを可能にするワークアラウンドである。
Requestableの作成
associated typeを使ったgenericなrequestableをまず作成する。
このままでは、このprotocolを変数として宣言することはできない。
protocol Requestable {
associatedtype ResponseType
func request(completion: @escaping (Result<ResponseType, Error>) -> Void)
}
Type EraserでRequestableをラップ
ここでいよいよType Erasureのテクニックが登場する。
class AnyRequestable<Model>: Requestable {
typealias ResponseType = Model
init<Inner: Requestable>(_ inner: Inner) where ResponseType == Inner.ResponseType {
self._request = inner.request
}
private let _request: ( (_ completion: @escaping (Result<Model, Error>) -> Void) -> Void )
func request(completion: @escaping (Result<Model, Error>) -> Void) {
self._request(completion)
}
}
このclass自体はgenericなクラスで、APIのレスポンスのモデルの型を設定できる。
先ほど作成したgenericなprotocolであるRequestableに準拠した何らかのクラスのインスタンスを初期化時に差し込む。そして差し込んだインスタンスのrequestableのメソッドをclosureとしてAnyRequestable内部で保持する。
そして次がトリッキーな部分だ。
AnyRequestable自身もRequestableに準拠している。そのためAnyRequestableにもRequestableのメソッドであるrequestメソッドが実装されている。AnyRequestable実装のrequestメソッドが呼ばれた場合、AnyRequestableが保持しているclosureのrequestを実装する。これはAnyRequestableが初期化時に保持した、外部から差し込まれたRequestableの実態のrequestメソッドである。
この一連の処理は注意深く読めば理解できると思う。
実際のrequestの作成
UserとBookに関するそれぞれのRequestを作成した。これらはAnyRequestableではなくRequestableに準拠している。
class UsersRequest: Requestable {
typealias ResponseType = [User]
func request(completion: @escaping (Result<[User], Error>) -> Void) {
// ここに実際にUserを取ってくる処理を書く
}
}
class BooksRequest: Requestable {
typealias ResponseType = [Book]
func request(completion: @escaping (Result<[Book], Error>) -> Void) {
// ここに実際にBookを取ってくる処理を書く
}
}
ViewControllerへの差し込み
ViewControllerは各requestをAnyRequestableとして保持。
外部からrequestを差し込む場合は、それぞれのrequestをAnyRequestableでラップしたものを差し込む。
class MyViewController: UIViewController {
var usersRequest: AnyRequestable<[User]>?
var booksRequest: AnyRequestable<[Book]>?
}
let myViewController = MyViewController()
/*
Users Requestの差し込み
AnyRequestableが[User]のgenericで宣言されている点に注目
さらに初期化時にAnyRequestableへusersRequestを差し込む。
myViewControllerへはanyUsersRequestを差し込む。
*/
let usersRequest = UsersRequest()
let anyUsersRequest = AnyRequestable<[User]>(usersRequest)
myViewController.usersRequest = anyUsersRequest
// Books Requestの差し込み: Usersと同じように差し込む
let booksRequest = BooksRequest()
let anyBooksRequest = AnyRequestable<[Book]>(booksRequest)
myViewController.booksRequest = anyBooksRequest
V. 参考文献
Type-Erased Wrappers in Swift
try! Swift: Type Erasureのユースケースを考えてみた話