7
8

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 3 years have passed since last update.

SwiftでType Erasureを使いgenericなAPI protocolを作成する

Last updated at Posted at 2019-10-29

I. はじめに

APIをViewControllerなどに持たせる場合、直接APIを持たせるのではなく、modelを取得するインターフェースを持つprotocolを作成し、それに準拠した実態としてのAPIをDIすると言うやり方で実装する人が多いのではないだろうか。

protocolをgenericにしてAPIのレスポンスの型を差し込み使いまわしたいところだが、protocolをgenericにしてしまうと、それを変数として宣言できなくなってしまうため、毎回個別にprotocolを定義しなければいけない。

今回はこの問題をType Erasureと言うテクニックを使い、genericだが変数として保持可能なprotocolを作成することで解決したと思う。

II. よくある実装例

protocolを作成

ユーザーを取得するインターフェースをprotocolとして作成

UsersRequestable.swift
protocol UsersRequestable {
    func fetch(completion: ((Result<[User], Error>) -> Void))
}

実態のAPIクラスを作成

本番用のものとStubの2つのAPIを先ほどのUsersRequestableへ準拠させて作成。

UsersRequest.swift
/// 実際にサーバーへ繋げるAPI
class UsersRequest: UsersRequestable {
    func fetch(completion: ((Result<[User], Error>) -> Void)) {
        // サーバーからデータを取ってくる
    }
}
StubUsersRequest.swift
/// 開発や自動化テスト用のStubを返すAPI
class StubUsersRequest: UsersRequestable {
    func fetch(completion: ((Result<[User], Error>) -> Void)) {
        // Stubの情報を返す
    }
}

ViewControllerへDI

先ほど作成したUsersRequestおよびStubUsersRequestは、ViewControllerへ状況に応じてDIされる。

UsersRequestableを変数として保持

実際に中に何が入っているかは感知しない。

MyViewController.swift
class MyViewController: UIViewController {
    var usersRequest: UsersRequestable?
}

状況に応じたAPIを差し込む

Configurator.swift
// 本番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を使えと言われてしまう。

AnyRequestable.swift

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はコンパイルを通る。

AnyRequestable.swift
// これはコンパイルを通る
protocol AnyRequestable {
    associatedtype T

    func fetch(completion: ((Result<[T], Error>) -> Void))
}

しかしAnyRequestableを変数として保持させようとするとコンパイルエラーになってしまう。

MyViewController.swift
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を変数として宣言することはできない。

Requestable.swift
protocol Requestable {
    associatedtype ResponseType
    func request(completion: @escaping (Result<ResponseType, Error>) -> Void)
}

Type EraserでRequestableをラップ

ここでいよいよType Erasureのテクニックが登場する。

AnyRequestable.swift
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に準拠している。

Requests.swift
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でラップしたものを差し込む。

Configurator.swift
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のユースケースを考えてみた話

7
8
0

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
7
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?