提供統一的錯誤API

快樂的小樑同學發表於2018-11-26

我想分享一種技術,在使用Swift do, try, catch error處理模型時,我發現它非常有用,可以限制從給定API呼叫中丟擲的錯誤數量。

目前,Swift不提供型別錯誤(在其他語言中稱為“已檢查的異常”,如Java),這意味著丟擲的任何函式都可能丟擲任何錯誤。雖然這為我們提供了很大的靈活性,但在使用API​​時,對於生產程式碼和測試來說,這也是一個挑戰。

考慮以下函式,該函式通過從URL同步載入資料來執行操作:

class AClass {
    enum SearchError: Error {
        case invalidParameters(String)
    }
    
    func loadData(_ parameters: String) throws -> Data {
        let urlString = "https://host/q=\(parameters)"
        
        guard let url = URL(string: urlString) else {
            throw SearchError.invalidParameters(parameters)
        }
        
        return try Data(contentsOf: url)
}
複製程式碼

正如您在上面所看到的,我們的函式可以結束在兩個不同的位置(嘗試構造URL時和使用URL 初始化Data時)。所以,這就是問題所在; 作為一個API使用者,我很不清楚我可以期待這個函式丟擲什麼樣的錯誤。我不需要知道這個函式在Data內部使用哪些型別,但我還需要知道Data初始化程式可以丟擲哪些錯誤。

在API設計中,必須瞭解imlpementation細節通常是一個不好的跡象,所以如果我們能保證我們的函式只丟擲SearchError型別的錯誤,那不是更好嗎?幸運的是,它很容易修復。我們所要做的就是將對資料的呼叫包裝在一個do, try, catch塊中。重構以後的程式碼,像這樣:

class AClass {
    enum SearchError: Error {
        case invalidParameters(String)
        case loadingFailed(URL)
    }
    
    func loadData(_ parameters: String) throws -> Data {
        let urlString = "https://host/q=\(parameters)"
        
        guard let url = URL(string: urlString) else {
            throw SearchError.invalidParameters(parameters)
        }
        
        do {
            return try Data(contentsOf: url)
        } catch {
            throw SearchError.loadingFailed(url)
        }
    }
}
複製程式碼

我們上面所做的是將初始化Data丟擲的異常替換為我們自己的錯誤。現在,我們可以記錄我們的函式總是丟擲一個SearchError,並且我們的API在錯誤處理方面變得更容易使用。

###然而,在使我們的API變得更好的同時,我們的實現也變得混亂了。通常,您需要使用do, try, catch塊包裝多個呼叫,這將使我們的程式碼很快變得難以閱讀。為了解決這個問題,我建立了一個簡單的函式來執行此包裝,並在丟擲基礎錯誤時丟擲特定錯誤。它看起來像這樣:

func perform<T>(_ expression: @autoclosure () throws -> T, orThrow error: Error) throws -> T {
    do {
        return try expression()
    } catch let error {
        throw error
    }
}
複製程式碼

###使用它,我們現在可以從新更新我們的loadData功能,使其更簡單:

func loadData(_ parameters: String) throws -> Data {
        let urlString = "https://host/q=\(parameters)"
        
        guard let url = URL(string: urlString) else {
            throw SearchError.invalidParameters(parameters)
        }
        
        return try perform(Data(contentsOf: url), orThrow: SearchError.loadingFailed(url))
    }
複製程式碼

我們現在有一個統一的錯誤API和一個更簡單的實現!

這種處理問題的思路,在Alamofire中也有體現,如JSONEncoding中的encode函式

public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
        var urlRequest = try urlRequest.asURLRequest()

        guard let parameters = parameters else { return urlRequest }

        do {
            let data = try JSONSerialization.data(withJSONObject: parameters, options: options)

            if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
                urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
            }

            urlRequest.httpBody = data
        } catch {
            throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
        }

        return urlRequest
    }
複製程式碼

DataRequest中Requestable也有所體現:

func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
            do {
                let urlRequest = try self.urlRequest.adapt(using: adapter)
                return queue.sync { session.dataTask(with: urlRequest) }
            } catch {
                throw AdaptError(error: error)
            }
        }
複製程式碼

如果您有任何問題,建議或反饋,請隨時與我聯絡~

相關文章