プロトコル指向でもっとに便利に! Alamofireを使ったAPIリクエスト設計

iOSアプリでサーバーとのAPI連携を行うとき、
皆さんはどのような設計で実装していますか?
きっと様々な方法があると思いますが、
リクエスト処理はできるだけシンプルに見通しを良くしておきたいですよね。

Protocolを使ってリクエストを綺麗に管理する

そこで今回はSwiftのProtocol機能とAlamofireを組み合わせて、
リクエスト処理を見やすく、綺麗に管理できる設計をご紹介したいと思います。

詳しい説明に入る前に、今回ご紹介する設計を利用すると以下のように書くことが出来ます。

(本記事のコードはSwift2.2で書かれています。
また、リクエストとレスポンスの処理の例としてSwiftyJSONを利用しています。)

リクエストの定義

struct GetUserListRequestContext: SampleAppRequestType, GETRequestContextType, JSONResponseType {    
  var path: String {
    return "/users"
  }

  var parameterJSON: SwiftyJSON.JSON {
    let json = JSON([
      "limit" : 10,
      "page" : 1,
    ])
    return json
  }
}

struct GetUserRequestContext: SampleAppRequestType, GETRequestContextType, JSONResponseType {
  let userID: String

  init(userID: String) {
    self.userID = userID
  }

  var path: String {
    return "/user/\(userID)"
  }
}

リクエストの実行コード

let context = GetUserRequestContext(userID: "abcd")
let request: Alamofire.Request = context.create { (response: Alamofire.Response<SwiftyJSON.JSON, NSError>) in
  // レスポンス
}
request.resume()

定義されているFooRequestContextというStructはAlamofireのRequestの作成だけを行い、
リクエストの実行やレスポンスのハンドリングはAlamofireの機能を利用するような設計にしています。

以下より、RequestContextについて詳しく説明していきます。

RequestContextの実装

それでは、先ほどのRequestContextが実際にどのように実装されているかを詳しく説明していきます。
まず、RequestContextは以下の3つのProtocolを実装しています。

  • リクエストに関わる RequestType Protocol
  • レスポンスに関わる ResponseType Protocol
  • Alamofire.Requestを作成する RequestContextType Protocol

それぞれの詳細を見ていきましょう。

  • リクエストに関わる RequestType Protocol
protocol RequestType {

  /// HTTP メソッド
  var method: Alamofire.Method { get }

  /// リクエストを送るURL
  var URLString: String { get }

  /// リクエストを行うAlamofire.Manager
  var manager: Alamofire.Manager { get }

  /// Alamofire.Requestを生成する処理
  func createRequest(method method: Alamofire.Method, URLString: String, manager: Alamofire.Manager) -> Alamofire.Request
}
  • レスポンスに関わる ResponseType Protocol
protocol ResponseType {

  associatedtype SerializedObject
  associatedtype ResponseError: ErrorType

  /// レスポンスのNSDataを扱いやすい型に変換する処理
  var responseSerializer: Alamofire.ResponseSerializer<SerializedObject, ResponseError> { get }
}
  • Alamofire.Requestを作成する RequestContextType Protocol

(このProtocolのextensionメソッドを使うためにはResponseType, RequestTypeを実装している必要があります。)

protocol RequestContextType {}

extension RequestContextType where Self: ResponseType, Self: RequestType {

  public func create(block: Alamofire.Response<SerializedObject, ResponseError> -> Void) -> Alamofire.Request {

    let request = self.createRequest(method: self.method, URLString: self.URLString, manager: self.manager)
    request.response(responseSerializer: self.responseSerializer, completionHandler: block)
    return request
  }
}

とりあえず、Protocolをそのまま実装してみる

とりあえず、先ほどの3つのProtocolをそのまま実装してみると、以下のようになります。
(例としてレスポンスはSwiftyJSONに変換するようにしています。)

struct SampleRequestContext: RequestContextType, RequestType, ResponseType {

  var method: Alamofire.Method {
    return .GET
  }

  var URLString: String {
    return "https://httpbin.org/get"
  }

  var manager: Manager {
    return Alamofire.Manager.sharedInstance
  }

  var responseSerializer: Alamofire.ResponseSerializer<SwiftyJSON.JSON, NSError> {
    return ResponseSerializer<SwiftyJSON.JSON, NSError> { request, response, data, error in

      // しっかりとしたエラーハンドリングはここでは割愛します。
      if let error = error {
        return .Failure(error)
      }
      let json = JSON(data: data!)
      return .Success(json)
    }
  }

  func createRequest(method method: Alamofire.Method, URLString: String, manager: Alamofire.Manager) -> Alamofire.Request {

    return manager.request(method, URLString, parameters: [“foo”:”bar”], encoding: .URL, headers: nil)
  }
}

Protocol Extensionsを使って処理を共通化し、コード量を削減する

整っているように見えますが、これらをリクエストごとに記述していくとすると、
リクエストごとのコード量がすごいことになってしまいます。
そこで、先ほどのコードから共通化できる部分をProtocol Extensionsとして実装していきます。

では、順番に説明していきたいと思います。

リクエストを行うAlamofire.ManagerをProtocol Extensionsへ

リクエストごとにAlamofire.Mangerを指定する必要はあまりなさそうなので共通化できそうですね。
これをRequestTypeを継承したDefaultManagerTypeとして実装します。
また、リクエストに使用するパラメータをJSONで指定できるようにしてみます。

/// RequestTypeを継承
protocol DefaultManagerType: RequestType {
  var parameterJSON: SwiftyJSON.JSON { get }
}

extension DefaultManagerType {    
  var manager: Manager {
    return Alamofire.Manager.sharedInstance
  }

  var parameterJSON: SwiftyJSON.JSON {
    // パラメータが不要な場合に省略可能にするためのデフォルト実装
    return JSON([:])
  }
}

レスポンス処理をProtocol Extensionsへ

次にレスポンスをJSONに変換する部分の共通化です。
多くの場合、レスポンスはJSONで返却されると思うので、リクエストごとにレスポンス処理を書く必要はなさそうです。
これをResponseTypeを継承したJSONResponseTypeとして実装します。

/// ResponseTypeを継承
protocol JSONResponseType: ResponseType {    
  var responseSerializer: Alamofire.ResponseSerializer<SwiftyJSON.JSON, NSError> {
    return ResponseSerializer<SwiftyJSON.JSON, NSError> { request, response, data, error in

      // ちゃんとしたハンドリングはここでは割愛します。
      if let error = error {
        return .Failure(error)
      }
      let json = JSON(data: data!)
      return .Success(json)
    }
  }
}

ここまでで、リクエストを実行する部分とレスポンスをJSONにする処理を共通化する準備が出来ました。

HTTPリクエストメソッドごとにProtocolを用意

最後にHTTPリクエストメソッドごとにProtocolを用意します。

コードが多くなってしまうため、本記事ではGETのみとしたいと思います。
RequestContextTypeと先ほど定義したDefaultManagerTypeを継承したGETRequestContextTypeとして実装します。

/// RequestContextType, DefaultManagerTypeを継承
protocol GETRequestContextType: RequestContextType, DefaultManagerType {}

extension GETRequestContextType where Self: DefaultManagerType {
  var method: Alamofire.Method {
    return .GET
  }

  func createRequest(method method: Alamofire.Method, URLString: String, manager: Alamofire.Manager) -> Alamofire.Request {

    let parameters = self.parameterJSON.dictionaryObject! // エラーハンドリングは割愛します。
    return manager.request(method, URLString, parameters: parameters, encoding: .URL, headers: nil)
  }
}

準備完了

これで準備は整いました。
定義したProtocolを使って先ほどのRequestContextを書き直してみます。

struct SampleRequestContext: GETRequestContextType, JSONResponseType {
  var URLString: String {
    return "https://httpbin.org/get"
  }

  var parameterJSON: SwiftyJSON.JSON {
    return JSON(["foo":"bar"])
  }
}

かなりコードを減らすことが出来ました。
これならリクエストが増えても見通しが良さそうです。
また、Structの宣言を見るだけてGETでJSONを受け取るということも分かるようになりました。

もう少し便利に

このついでに、もう少し便利にしたいと思います。
APIのホストを固定して、パスのみで記述できるようにします。
以下のProtocolを定義します。

// RequestTypeを継承
protocol SampleAppRequestType: RequestType {
  var path: String { get }
}

extension SampleAppRequestType {
  var URLString: String {
    // アプリごとのBaseURLとPathを連結するようにします。
    return "https://httpbin.org" + self.path
  }
}

そして、先ほどのSampleRequestContextにSampleAppRequestTypeを採用すると、以下のようになります。

struct SampleRequestContext: SampleAppRequestType, GETRequestContextType, JSONResponseType {
  var path: String {
    return "/get"
  }

  var parameterJSON: SwiftyJSON.JSON {
    return JSON(["foo":"bar"])
  }
}

完成

以上で完成となります。
Protocol Extensionsにより、RequestContextひとつあたりのコードをぐっと減らすことが出来ました。

この設計のメリット

この設計のメリットはProtocolベースの設計なので自由に拡張が出来ることです。
そのため、アプリの要件に合わせた柔軟なカスタマイズが可能になります。
また、Protocolの定義次第ではRequestContextの宣言を見るだけでどのようなリクエストなのか分かるようになるのもポイントです。

例えば、以下のように宣言されているとします。

struct SampleRequestContext: GETRequestContextType, JSONResponseType, DefaultManagerType

struct SampleRequestContext: GETRequestContextType, ImageResponseType, DefaultManagerType

struct SampleRequestContext: GETRequestContextType, JSONResponseType, BackgroundManagerType

このようなProtocolにしておくと、
– HTTPリクエストメソッドは?
– レスポンスはどうなる?
– フォアグラウンド通信? バックグラウンド通信?

という感じに一目見るだけで概要を把握することが出来る状態を作ることができます。

そして、この設計のもう一つのメリットは、
Alamofire.Requestの生成のみを行うため、Alamofireの機能を全て利用できることです。
Alamofireはもはやデファクトと言える素晴らしいライブラリなので、
余計にラップしてしまうのはもったいないのではないかと考えています。

冒頭でご紹介した3つのProtocolはBrickRequestとして公開しています。

BrickRequestについて

BrickRequestは本記事で載せている3つのProtocolのコードのみとなっています。

その理由として、
アプリとサーバーの連携方法はサービスによって微妙に違うことも多く、
ガチガチに便利なライブラリを作っても、他のアプリに持って行った時に都合が悪くて色々直すことに…
などうまくいかないことが多いと思います。(リクエストに限った話ではないと思いますが)
そこで、設計と考え方だけをライブラリとして、そこからアプリに合わせたカスタマイズを行えるようにするため、実装をあえて含めていません。

サンプルの実装としてBrickRequestで簡単にリクエストを行えるRESTRequestも公開しています。
(RESTRequestではRxSwiftを利用して、通信に失敗した際の自動リトライ機能も実装しています。)

なぜBrick?

BrickRequestのネーミングですが、
共通化したい処理をProtocolに分離して、それらを組み合わせてリクエストを作るというところから、
LEGOブロックを組み合わせるイメージに例えてBrickとしています。

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

【イベントレポート】 DMM.comラボさん・インテリジェンスさんとの合同勉強会を開催しました

Go言語とGoogle Cloud Vision APIで画像認識