はじめに
概要
責務の分離とprotocolとDIによる特定の責務に依存しない柔軟なAPIリクエスト用のネットワークレイヤー(APINetworkLayer)を作成した。
特に実際にリクエストを送信する部分をURLSessionやAlamofireなどを簡単にDIで切り替え可能で、jsonのパースも同様に簡単に実装方法を切り替えられることを意識して作成している。
この記事ではこのネットワークレイヤーの解説と使い方を記載する。
APIの作成を簡単かつ柔軟にするため少々複雑な部分もあるが、使用例等を含めて全体を見通せば理解できると思う。
すぐに試したい人はAPINetworkLayerのコードをプロジェクトに取り込み、後半の「プロジェクト側での実装」のコードを使えば簡単に動作させることも可能である。
注意
- 便宜上structを使った方が適切な場合もclassと表現している
- エラーハンドリングは今回の本質とはずれるのでしっかり書いていない
- HTTPHeaderに関する詳細は割愛している
4つの主要なコンポーネント
APINetworkLayer内のフォルダとファイルの構造は以下の通りになる。 [必須]と記載しているもの以外は実装を便利にしたり一部機能をDIにより差し替えるために記載しているもので無くても良い。
- APIType
- APIType.swift [必須]
- Request.swift [必須]
- ResponseSerializer.swift [必須]
- Serializer
- Serializable.swift [必須]
- CodableSerializer.swift
- Networking
- APIType+dispatch.swift [必須]
- RequestDispatchable.swift [必須]
- APIError.swift [必須]
- RequestDispatcher
- URLSessionRequestDispatcher.swift [必須]
- AlamofireRequestDispatcher.swift
- StubRequestDispatcher.swift
1. API Type
APITypeは全てのスタートポイントでもっとも重要な部分である。APINetworkLayer全体もここを中心に設計されている。DDD的な言い方をすればコアドメインのようなもの。
これを独自に定義したAPI(ex. UserAPI)などのclassへ準拠させてAPIを作成する(正確にはほとんどのケースで一部共通処理を内包したAPIBaseType等を定義して、直接はそれをAPIのclassへ準拠させて使う。)
Swiftプロジェクトにおいて定義されるAPIは主に2つの責務を担っている。
一つはHTTPリクエストを送信すること、もう一つは帰ってきたレスポンンスからModel等のデータをパースして取り出すことである。これらの2つ含むprotocolをAPITypeとして定義すると以下のようになる。
public protocol APIType {
associatedtype ResponseType
var request: Request { get }
var responseSerializer: ResponseSerializer { get }
}
また返すmodelの型のタイプをResponseTypeとして保持している。
Request
APITypeのpropertyの内、HTTPリクエストを送る際に必要な情報を保持。SwiftのURLRequestのAPIリクエストに必要な情報のみを抜粋したようなもの。
public struct Request {
// Property
public let url : String
public let method : HTTPMethod
public let params : [String: Any?]?
public let headers: [String: String]?
// Init
public init (
url : String,
method : HTTPMethod,
params : [String: Any?]? = nil,
headers: [String: String]? = nil
) {
self.url = path
self.method = method
self.params = params
self.headers = headers
}
}
public enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
case patch = "PATCH"
}
ResponseSerializer
HTTPリクエストから帰ってきたレスポンスをmodelに変換する部分。
全てのAPIにおいて共通しているのは、Data形式のレスポンスを受け取り、をれを指定した型のモデルへ変換して返すということである。
これをclosureとして宣言してtypealiasに差し込みそれがどのようなものかの名前を与える。
public extension APIType {
typealias ResponseSerializer = ((Data) -> (ResponseType?))
}
ResponseSerializerへdataからmodelへ変換する処理が書かれているfunctionを差し込み、差し込まれたclosureが実際のdata -> modelへの変換処理を行う。
ResponseTypeはAPITypeで先ほど宣言したassociatedtypeでありAPITypeを準拠させた具体的なAPI class等で型が差し込まれる。genericにどのような型へも対応可能である。
dataを受けっとって特定の型を返すfunctionならあらゆるものが差し込めるため、serializeの実装方法に依存せずどのよな方法でのserializationの実装にも対応できる。
closureの内部がCodableで処理されていようと、SwiftyJSONで処理されていようと、Jsonを[String: Any]へ変換して手動で処理していようとAPITypeとResponseSerializerはそれを感知しない。
よってプロジェクトの事情等に応じてserializationの方法を途中で差し替えるのも容易である。
APITypeを使ってAPIを作成する例
実際にAPITypeを使いAPIを作成した場合はこのようになる。
import APINetworkRequest
/**
理解するためのサンプルなので現実のプロジェクトではすこし違った実装になる。
現実のプロジェクトではBaseURLなど全てのAPIに共通する処理があるため、
APITypeを直接特定のAPIへ準拠はさせずBaseAPIType等処理を共通化したprotocolを容易する。
*/
struct SimpleUserAPI: APIType {
typealias ResponseType = [User]
var request: Request = Request(url: "https://my-sample-site.co.jp/api/users",
method: .get,
params: nil,
headers: nil)
var responseSerializer: ((Data) -> ([User]?)) = { data in
// ここにData型からResponseType(= [User])へ変換する処理を差し込む。
// ほとんどの場合は他の場所で定義した汎用のSerializationコードを差し込むことになる。
// これに関してはSerializerの項で解説
}
}
2. Serializer
APITypeの項で全てのAPIはHTTPのレスポンスのData型のobjectを特定の型のmodelへ変換する必要があると解説した。Serialization配下にはこの処理に特化したパーツが配置されている。
Serializable
protocol Serializableではdataを受け取りgenericな指定された型を返すfunctionが定義されている。
public protocol Serializable {
associatedtype SerializingType
static func serialize(data: Data) -> SerializingType?
}
これを準拠させたたclassのserializeメソッドはAPITypeのresponseSerializerへ差し込むことが可能である。これらをAPITypeに差し込むことでAPIからserializationの処理を分離できる。
data -> modelの変換さえ行われていればその処理の詳細は何でもよくAPITypeからは処理が隠蔽され、Serializer classでは必要に応じて処理の方法をCodable, SwiftyJSON等選択&変更が可能であり、それをAPIへは影響を与えず行える。
CodableSerializer
必須ではないが、Codableを使ったSerializerを簡単に作成できる汎用struct。
以下のような書き方で簡単に特定のCodable準拠のmodelのserializeメソッドを作成できる。
public struct CodableSerializer<T: Codable>: Serializable {
public typealias SerializingType = T
public static func serialize(data: Data) -> T? {
do {
let jsonDecoder = JSONDecoder()
let serialized = try jsonDecoder.decode(SerializingType.self, from: data)
return serialized
} catch {
return nil
}
}
}
APITypeに準拠したAPIのresponseSerializerの部分へserializeメソッドを差し込むこことで使用する。
// SimpleUserAPI内での実装例
var responseSerializer: ((Data) -> ([User]?)) = CodableSerializer<[User]>.serialize
Serializableを使ったSerializerの作成例
struct UsersSerializer: Serializable {
typealias SerializingType = [User]
// data -> [User]の変換さえ行われればどのような実装でも良い。
// 2つの例を記載。サンプル用にserializeを複数回宣言しているのでコンパイル不可なので注意
/**
手動でのjsonパースの例
通常はCodable等を使うのでこのような実装を行うことは少ない。
*/
static func serialize(data: Data) -> SerializingType? {
guard let json = data as? [[String: Any]] else { return nil }
return json.map { User(json: $0) }
}
/**
CodableSerializerを使った例。
CodableSerializerを直接APIに差し込むより、ここで定義した方がDRY原則に
沿っていておすすめ。
*/
static func serialize(data: Data) -> SerializingType? {
return CodableSerializer<SerializingType>.serialize(data: data)
}
}
3. Networking
Networking配下には実際にRequestを送信したりそのレスポンスを処理した理するためのコンポーネントが配置されている。
APIType+dispatch
protocol APITypeに実際HTTPリクエストを送信させるメソッドを追加。Dispatcherは先に定義したRequestオブジェクトを使いHTTPリクエストを行いdataをレスポンスとして返す。
DispatcherはDI可能で実際のdispatch処理はどのように行われようとAPITypeは感知しない。URLSession, Alamofire, テスト用のStubなど様々な方法の実装に簡単に差し替えが可能である。
Dispatcherから帰ってきたDataはAPITypeが保持しているserializerメソッドでModelへ変換される。
public extension APIType {
// デフォルトでは後述のURLSessionRequestDispatcherを使うようにしている
public func dipatch(dispatcher: RequestDispatchable = URLSessionRequestDispatcher(),
onSuccess: @escaping (ResponseType) -> Void,
onError: @escaping (Error) -> Void) {
dispatcher.dispatch(request: self.request,
// Success
onSuccess: { (responseData: Data) in
guard let parsedData = self.responseSerializer(responseData) else {
onError(APIError.responseParseFailed)
return
}
DispatchQueue.main.async {
onSuccess(parsedData)
}
},
// Erorr
onError: { (error: Error) in
DispatchQueue.main.async {
onError(error)
}
}
)
}
}
RequestDispatchable
APIType+dispatchのdispatchメソッドの部分でDispatcherをDI可能にするためのprotocol。
public protocol RequestDispatchable {
func dispatch(request: Request,
onSuccess: @escaping (Data) -> Void,
onError: @escaping (Error) -> Void)
}
APIError
エラーは本記事の本質からはずれるのでかなり簡略に書いている。実際にプロジェクトで使う場合は必要なcase等を追加。
public enum APIError: Swift.Error {
case urlNotValid
case noResponseData
case responseParseFailed
}
4. Request Dispatcher
Request DispatcherにはRequestDispatchableに準拠した複数のdispatcherは定義されている。
ここでは3つ方法での実装を解説するが、実際のプロジェクトではおそらく全ては必要にならない。
URLSessionRequestDispatcher
ライブラリ等に頼らずURLSessionを使ってHTTPリクエストを実装するクラス。
public struct URLSessionRequestDispatcher: RequestDispatchable {
public init() {}
public func dispatch(request: Request,
onSuccess: @escaping (Data) -> Void,
onError: @escaping (Error) -> Void) {
guard let urlRequest = createURLRequest(by: request, onError: onError) else { return }
URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
// Error
if let error = error {
onError(error)
return
}
// Check response error
guard let data = data else {
onError(APIError.noResponseData)
return
}
// Success
onSuccess(data)
}.resume()
}
private func createURLRequest(by restURLRequest: Request, onError: (Error) -> Void) -> URLRequest? {
// Create URL Request
guard let url = URL(string: restURLRequest.url) else {
onError(APIError.noResponseData)
return nil
}
var urlRequest = URLRequest(url: url)
// Set HTTP Method
urlRequest.httpMethod = restURLRequest.method.rawValue
// Set Param
do {
if let params = restURLRequest.params {
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: params, options: [])
}
} catch let error {
onError(error)
return nil
}
// Set Headers
if let headers = restURLRequest.headers {
urlRequest.allHTTPHeaderFields = headers
}
return urlRequest
}
}
AlamofireRequestDispatcher
Alamofireを使った例
import Alamofire
public struct AlamofireRequestDispatcher: RequestDispatchable {
public init() {}
public func dispatch(request: Request, onSuccess: @escaping (Data) -> Void, onError: @escaping (Error) -> Void) {
let afRequest: DataRequest
switch request.method {
case .get:
afRequest = AF.request(request.url, method: .get, parameters: request.params)
case .post:
afRequest = AF.request(request.url, method: .get, parameters: request.params)
case .delete:
afRequest = AF.request(request.url, method: .get, parameters: request.params)
case .patch:
afRequest = AF.request(request.url, method: .get, parameters: request.params)
case .put:
afRequest = AF.request(request.url, method: .get, parameters: request.params)
}
afRequest.validate()
.responseJSON { response in
guard response.result.isSuccess else {
print("")
onError(APIError.generalError)
return
}
guard let data = response.data else {
onError(APIError.noResponseData)
return
}
onSuccess(data)
}
}
}
StubRequestDispatcher
テスト等で使うためのStubの例
public struct StubRequestDispatcher: RequestDispatchable {
public func dispatch(request: Request, onSuccess: @escaping (Data) -> Void, onError: @escaping (Error) -> Void) {
onSuccess(Data())
}
public init() {}
}
プロジェクト側での実装
Codable準拠のUser modelのAPIを作成するという前提でのサンプルコードを記載。
struct User: Codable {
let id: Int
let name: String
}
BaseAPI
base URLの共通化とenumを使った実際のAPIクラスでselfでのswitch分を書きやすくするための処理。
実際に作成するAPIへはこのprotocolを準拠させる。APITypeのWrapper。
import APINetworkRequest
protocol BaseAPIType: APIType {
var httpMethod: HTTPMethod { get }
var path: String { get }
var params: [String: Any] { get }
}
extension BaseAPIType {
var url: String {
let baseURL = "https://my-sample-site.co.jp/"
return baseURL + self.path
}
var request: Request {
return Request(url: self.url,
method: self.httpMethod,
params: self.params,
headers: nil)
}
}
UserSerializer
CodableSerializerを使ったUserSerializerを定義。
import APINetworkRequest
struct UsersSerializer: Serializable {
typealias SerializingType = [User]
static func serialize(data: Data) -> SerializingType? {
return CodableSerializer<SerializingType>.serialize(data: data)
}
}
UserAPI
enumへBaseAPITypeを準拠させることで複数のUserAPIを一括で管理させる。
必ずしもenumへ準拠させる必要はなくstructなどでも問題ない。
import APINetworkRequest
enum UserAPI: BaseAPIType {
case all
case latest(requestParam: [String: Any]) // paramは本来もっとしっかり書くべき
typealias ResponseType = [User]
var path: String {
switch self {
case .all: return "users"
case .latest: return "users/latest"
}
}
var params: [String : Any] {
switch self {
case .all:
return [:]
case .latest(let requestParam):
return requestParam
}
}
var httpMethod: HTTPMethod {
return .get
}
var responseSerializer: ((Data) -> ([User]?)) {
// 先に別途定義したUserSerializerを使用
return UsersSerializer.serialize
}
}
実際のAPIの呼び出し
呼び出し時はdispatcherをDI可能。この部分までくると一般的なAPIの実装と大きな違いはない。
// 適当なparameterを準備
let latestUsersRequestParam: [String: Any] = [
"id": 1,
"type": "mytype"
]
/*
実際の呼び出し。
dispatcherは指定しなくてもデフォルトのものが差し込まれるが、
例としてわかりやすくするために明示的に指定。
AlamofireRequestDispatcherなど他のdispatcherをDIすることも可能。
*/
UserAPI.latest(requestParam: latestUsersRequestParam).dipatch(
dispatcher: URLSessionRequestDispatcher(),
onSuccess: { users in
print(users)
},
onError: { error in
// エラー処理
})