2
3

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

Swiftでprotocol指向なDIによる特定部分に依存しない自作APIネットワークレイヤー

Last updated at Posted at 2019-03-18

はじめに

概要

責務の分離と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として定義すると以下のようになる。

APIType.swift
public protocol APIType {
    associatedtype ResponseType
    
    var request: Request { get }
    var responseSerializer: ResponseSerializer { get }
}

また返すmodelの型のタイプをResponseTypeとして保持している。

Request

APITypeのpropertyの内、HTTPリクエストを送る際に必要な情報を保持。SwiftのURLRequestのAPIリクエストに必要な情報のみを抜粋したようなもの。

Request.swift
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に差し込みそれがどのようなものかの名前を与える。

ResponseSerializer.swift
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を作成した場合はこのようになる。

SimpleUserAPI.swift
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が定義されている。

Serializable.swift
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メソッドを作成できる。

CodableSerializer.swift
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.swift
// SimpleUserAPI内での実装例
var responseSerializer: ((Data) -> ([User]?)) = CodableSerializer<[User]>.serialize

Serializableを使ったSerializerの作成例

UsersSerializer.swift
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へ変換される。

APIType+dispatch.swift

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。

RequestDispatchable.swift
public protocol RequestDispatchable {
    func dispatch(request: Request,
                  onSuccess: @escaping (Data) -> Void,
                  onError: @escaping (Error) -> Void)
}

APIError

エラーは本記事の本質からはずれるのでかなり簡略に書いている。実際にプロジェクトで使う場合は必要なcase等を追加。

APIError.swift
public enum APIError: Swift.Error {
    case urlNotValid
    case noResponseData
    case responseParseFailed
}

4. Request Dispatcher

Request DispatcherにはRequestDispatchableに準拠した複数のdispatcherは定義されている。
ここでは3つ方法での実装を解説するが、実際のプロジェクトではおそらく全ては必要にならない。

URLSessionRequestDispatcher

ライブラリ等に頼らずURLSessionを使ってHTTPリクエストを実装するクラス。

URLSessionRequestDispatcher.swift
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を使った例

AlamofireRequestDispatcher.swift
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の例

StubRequestDispatcher.swift
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を作成するという前提でのサンプルコードを記載。

User.swift
struct User: Codable {
    let id: Int
    let name: String
}

BaseAPI

base URLの共通化とenumを使った実際のAPIクラスでselfでのswitch分を書きやすくするための処理。
実際に作成するAPIへはこのprotocolを準拠させる。APITypeのWrapper。

BaseAPI.swift
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を定義。

UserSerializer.swift
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などでも問題ない。

UserAPI.swift
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の実装と大きな違いはない。

APICallSample.swift
        // 適当な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
                // エラー処理
        })

参考文献

2
3
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
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?