如何优雅地处理使用带有闭包的 NSURLSessionTask 的方法中的错误?

Posted

技术标签:

【中文标题】如何优雅地处理使用带有闭包的 NSURLSessionTask 的方法中的错误?【英文标题】:How to elegantly handle errors in methods using NSURLSessionTask with closures? 【发布时间】:2015-12-29 15:30:49 【问题描述】:

当我有一个处理与 REST API 通信的类时,我经常有这样的代码,其中有很多方法使用带有闭包的 NSURLSession 任务作为回调。我还给其中一些方法提供了回调闭包,这样我就可以一个接一个地链接不同的 API 调用。我经常从视图控制器中调用这些方法,例如作为按下按钮后的操作或在 viewDidLoad 中。

让我们举一个示例类,它与一个虚构的 API 对话,其中一种方法发送 POST 请求以向 API 注册新用户:

class ApiConnection 
    var session: NSURLSession!

    init() 
        let config = NSURLSessionConfiguration.defaultSessionConfiguration()
        config.URLCache = NSURLCache.sharedURLCache()
        self.session = NSURLSession(configuration: config)
    


    func registerNewUser(user user: String, password: String, callback: (() -> Void)? ) 
        let data = ["email": user, "password": password]
        let url = NSURL(string: "https://backend.myapp.com/auth/register/")
        let request = NSMutableURLRequest(URL: url!)
        request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
        try! request.HTTPBody = NSJSONSerialization.dataWithJSONObject(data, options: NSJSONWritingOptions())

        let task = session.dataTaskWithRequest(request) 
            data, response, error in

            if (error != nil) 
                print("TODO do the right thing here for error: \(error!.localizedDescription)")
                return
            

            if let httpResponse = response as? NSHTTPURLResponse 

                do 
                    let result = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions()) as? NSDictionary

                    if httpResponse.statusCode == 201 

                        print("registration went successfully")
                        callback?()

                     else 

                        print("registration went wrong, not registered new user: \(result)")

                    



                
                catch 
                    print("TODO something went wrong decoding the JSON response")
                
            


        
        task.resume()
    


现在,例如在视图控制器中,我可以使用以下方式调用此方法:

let myAPI = ApiConnection()
myAPI.registerNewUser(user: "email@foo.bar", password: "testing") 
    // continue here, probably doing the next call
    print("user is registered")

到目前为止一切都很好,但是这个例子完全缺乏适当的错误处理(上面代码中的“TODO”,为了显示警告对话框或其他什么。我想知道如何改进我的代码以优雅的方式做到这一点.

当然,我可以在我创建的完成闭包中返回一个错误对象,并在视图控制器中做一些if error != nil 魔术,但这会很丑陋并且对于连续调用会变得很麻烦。

我认为新的 do-try-catch 习惯用法可能是完美的:拥有一个仅在成功情况下执行的完成闭包,除此之外,当出现任何问题时捕获异常,即使有我可以在 APIConnection 类中编写格式良好的错误消息。但是经过一些阅读后,我发现抛出一个闭包并不容易,因为这个闭包可以随时调用,即使在方法本身已经执行之后,这对我来说很有意义,但我不知道如何很好地处理这种情况。

经验丰富的程序员如何处理这个问题?我的 APIConnection 类的整个方法和体系结构是否愚蠢,整个事情可以做得更优雅? Plaese,感谢任何评论。

【问题讨论】:

您是否考虑过使用 AFNetworking? 说实话,还没有。 AFNetworking 如何更好地解决这个问题?在使用 Swift 2 时我应该使用 Alamofire 而不是 AFNetworking 吗? 我建议使用像 BrightFutures 或 FutureLib 这样的“Futures and Promises”库。 【参考方案1】:

最适合这类问题的是promises 概念。您在代码中执行的操作很容易被视为管道操作:get_server_respose |解析_json |执行回调。在同步方法中,这将是非常简单的编写并且将提供干净/可读的代码。在异步世界中,事情变得复杂,您很容易迷失在callback hell 中。

使用 Promise,您可以以与管道同步版本非常相似的方式链接异步操作,并且在链的末端只能有一个故障点。

这是您的问题的promise-d 版本的外观:

let task =  session.dataTaskWithRequest(request)
task.promisify.then(response, data in        
    guard let httpResponse = response as? NSHTTPURLResponse else 
       return failedPromise(Error(...)
    
    guard response.statusCode == 201 else 
       return failedPromise(Error(...)
    
    return jsonParsePromise(data);
).then( parsedDict in
    print("Registration went successfully")
    callback?()
).then(nil,  error in
    print("An error occured: \(error)")
)
task.resume()

更简洁,像 json 解析这样的任务在一个单独的(可测试的)单元中完成,并且只有一个故障点。

附:你可以更进一步,承诺 registerNewUser 函数本身:

func registerNewUser(user user: String, password: String) -> Promise 
    //... task setup
    return task.promisify.then(response, data in        
        guard let httpResponse = response as? NSHTTPURLResponse else 
           return failedPromise(Error(...)
        
        guard response.statusCode == 201 else 
           return failedPromise(Error(...)
        
        return jsonParsePromise(data);
    )
    task.resume()

,该函数的客户端可以方便地使用:

registerNewUser(usernameTextfield.text, passwordTextfied.text).then(user in
    print("Registration succeeded with user \(user)")
, error in
    print("Registration failed with error \(error)")
)

【讨论】:

你在上面的例子中使用 PromiseKit 吗?我真的认为 Promises 的概念很有意义,我想学习使用它,因此我想知道您为 Swift2 推荐的框架。 我目前正在使用我自己的框架之一,CKPromise。它是用 Objective-C 编写的,支持从 Swift 调用,我打算用 Swift 重写它。如果你想了解更多关于 Promise 的知识,也可以阅读我的博客文章 here 看了一圈之后,我现在已经通过使用 PromiseKit 解决了我的问题。 Swift 2 并没有完美地记录它,但效果很好,并且使编程所有类型的异步东西变得非常容易。顺便说一句,我还更改了所有 HTTP 请求以使用 Alamofire 和 SwiftyJSON。真的很不错的组合。

以上是关于如何优雅地处理使用带有闭包的 NSURLSessionTask 的方法中的错误?的主要内容,如果未能解决你的问题,请参考以下文章

如何优雅地处理时区

SpringBoot:如何优雅地处理全局异常?

Flux/Alt 数据依赖,如何优雅地道地处理

如何优雅地处理 SIGTERM 信号?

Auth0 邮件确认,如何优雅地处理注册

SpringBoot:如何优雅地进行参数传递响应数据封装异常处理?