[譯] 探究 Swift 中的 Futures & Promises

a_tuooo發表於2017-09-06

探究 Swift 中的 Futures & Promises

非同步程式設計可以說是構建大多數應用程式最困難的部分之一。無論是處理後臺任務,例如網路請求,在多個執行緒中並行執行重操作,還是延遲執行程式碼,這些任務往往會中斷,並使我們很難除錯問題。

正因為如此,許多解決方案都是為了解決上述問題而發明的 - 主要是圍繞非同步程式設計建立抽象,使其更易於理解和推理。對於大多數的解決方案來說,它們都是在"回撥地獄"中提供幫助的,也就是當你有多個巢狀的閉包為了處理同一個非同步操作的不同部分的時候。

這周,讓我們來看一個這樣的解決方案 - Futures & Promises - 讓我們開啟"引擎蓋",看看它們是如何工作的。。

A promise about the future

當介紹 Futures & Promises 的概念時,大多數人首先會問的是 Future 和 Promise 有什麼區別?。在我看來,最簡單易懂的理解是這樣的:

  • Promise 是你對別人所作的承諾。
  • Future 中,你可能會選擇兌現(解決)這個 promise,或者拒絕它。

如果我們使用上面的定義,Futures & Promises 變成了一枚硬幣的正反面。一個 Promise 被構造,然後返回一個 Future,在那裡它可以被用來在稍後提取資訊。

那麼這些在程式碼中看起來是怎樣的?

讓我們來看一個非同步的操作,這裡我們從網路載入一個 "User" 的資料,將其轉換成模型,最後將它儲存到一個本地資料庫中。用”老式的辦法“,閉包,它看起來是這樣的:

class UserLoader {
    typealias Handler = (Result<User>) -> Void

    func loadUser(withID id: Int, completionHandler: @escaping Handler) {
        let url = apiConfiguration.urlForLoadingUser(withID: id)

        let task = urlSession.dataTask(with: url) { [weak self] data, _, error in
            if let error = error {
                completionHandler(.error(error))
            } else {
                do {
                    let user: User = try unbox(data: data ?? Data())

                    self?.database.save(user) {
                        completionHandler(.value(user))
                    }
                } catch {
                    completionHandler(.error(error))
                }
            }
        }

        task.resume()
    }
}複製程式碼

正如我們可以看到的,即使有一個非常簡單(非常常見)的操作,我們最終得到了相當深的巢狀程式碼。這是用 Future & Promise 替換之後的樣子:

class UserLoader {
    func loadUser(withID id: Int) -> Future<User> {
        let url = apiConfiguration.urlForLoadingUser(withID: id)

        return urlSession.request(url: url)
                         .unboxed()
                         .saved(in: database)
    }
}複製程式碼

這是呼叫時的寫法:

let userLoader = UserLoader()
userLoader.loadUser(withID: userID).observe { result in
    // Handle result
}複製程式碼

現在上面的程式碼可能看起來有一點黑魔法(所有其他的程式碼去哪了?!?),所以讓我們來深入研究一下它是如何實現的。

探究 future

就像程式設計中的大多數事情一樣,有許多不同的方式來實現 Futures & Promises。在本文中,我將提供一個簡單的實現,最後將會有一些流行框架的連結,這些框架提供了更多的功能。

讓我們開始探究下 Future 的實現,這是從非同步操作中公開返回的。它提供了一種只讀的方式來觀察每當被賦值的時候以及維護一個觀察回撥列表,像這樣:

class Future<Value> {
    fileprivate var result: Result<Value>? {
        // Observe whenever a result is assigned, and report it
        didSet { result.map(report) }
    }
    private lazy var callbacks = [(Result<Value>) -> Void]()

    func observe(with callback: @escaping (Result<Value>) -> Void) {
        callbacks.append(callback)

        // If a result has already been set, call the callback directly
        result.map(callback)
    }

    private func report(result: Result<Value>) {
        for callback in callbacks {
            callback(result)
        }
    }
}複製程式碼

生成 promise

接下來,硬幣的反面,PromiseFuture 的子類,用來新增解決*拒絕*它的 API。解決一個承諾的結果是,在未來成功地完成並返回一個值,而拒絕它會導致一個錯誤。像這樣:

class Promise<Value>: Future<Value> {
    init(value: Value? = nil) {
        super.init()

        // If the value was already known at the time the promise
        // was constructed, we can report the value directly
        result = value.map(Result.value)
    }

    func resolve(with value: Value) {
        result = .value(value)
    }

    func reject(with error: Error) {
        result = .error(error)
    }
}複製程式碼

正如你看到的,Futures & Promises 的基本實現非常簡單。我們從使用這些方法中獲得的很多神奇之處在於,這些擴充套件可以增加連鎖和改變未來的方式,使我們能夠構建這些漂亮的操作鏈,就像我們在 UserLoader 中所做的那樣。

但是,如果不新增用於鏈式操作的api,我們就可以構造使用者載入非同步鏈的第一部分 - urlSession.request(url:)。在非同步抽象中,一個常見的做法是在 SDK 和 Swift 標準庫之上提供方便的 API,所以我們也會在這裡做這些。request(url:) 方法將是 URLSession 的一個擴充套件,讓它可以用作基於 Future/Promise 的 API。

extension URLSession {
    func request(url: URL) -> Future<Data> {
        // Start by constructing a Promise, that will later be
        // returned as a Future
        let promise = Promise<Data>()

        // Perform a data task, just like normal
        let task = dataTask(with: url) { data, _, error in
            // Reject or resolve the promise, depending on the result
            if let error = error {
                promise.reject(with: error)
            } else {
                promise.resolve(with: data ?? Data())
            }
        }

        task.resume()

        return promise
    }
}複製程式碼

我們現在可以通過簡單地執行以下操作來執行網路請求:

URLSession.shared.request(url: url).observe { result in
    // Handle result
}複製程式碼

鏈式

接下來,讓我們看一下如何將多個 future 組合在一起,形成一條鏈 — 例如當我們載入資料時,將其解包並在 UserLoader 中將例項儲存到資料庫中。

鏈式的寫法涉及到提供一個閉包,該閉包可以返回一個新值的 future。這將使我們能夠從一個操作獲得結果,將其傳遞給下一個操作,並從該操作返回一個新值。讓我們來看一看:

extension Future {
    func chained<NextValue>(with closure: @escaping (Value) throws -> Future<NextValue>) -> Future<NextValue> {
        // Start by constructing a "wrapper" promise that will be
        // returned from this method
        let promise = Promise<NextValue>()

        // Observe the current future
        observe { result in
            switch result {
            case .value(let value):
                do {
                    // Attempt to construct a new future given
                    // the value from the first one
                    let future = try closure(value)

                    // Observe the "nested" future, and once it
                    // completes, resolve/reject the "wrapper" future
                    future.observe { result in
                        switch result {
                        case .value(let value):
                            promise.resolve(with: value)
                        case .error(let error):
                            promise.reject(with: error)
                        }
                    }
                } catch {
                    promise.reject(with: error)
                }
            case .error(let error):
                promise.reject(with: error)
            }
        }

        return promise
    }
}複製程式碼

使用上面的方法,我們現在可以給 Savable 型別的 future 新增一個擴充套件,來確保資料一旦可用時,能夠輕鬆地儲存到資料庫。

extension Future where Value: Savable {
    func saved(in database: Database) -> Future<Value> {
        return chained { user in
            let promise = Promise<Value>()

            database.save(user) {
                promise.resolve(with: user)
            }

            return promise
        }
    }
}複製程式碼

現在我們來挖掘下 Futures & Promises 的真正潛力,我們可以看到 API 變得多麼容易擴充套件,因為我們可以在 Future 的類中使用不同的通用約束,方便地為不同的值和操作新增方便的 API。

轉換

雖然鏈式呼叫提供了一個強大的方式來有序地執行非同步操作,但有時你只是想要對值進行簡單的同步轉換 - 為此,我們將新增對轉換的支援。

轉換直接完成,可以隨意地丟擲,對於 JSON 解析或將一種型別的值轉換為另一種型別來說是完美的。就像 chained() 那樣,我們將新增一個 transformed() 方法作為 Future 的擴充套件,像這樣:

extension Future {
    func transformed<NextValue>(with closure: @escaping (Value) throws -> NextValue) -> Future<NextValue> {
        return chained { value in
            return try Promise(value: closure(value))
        }
    }
}複製程式碼

正如你在上面看到的,轉換實際上是一個鏈式操作的同步版本,因為它的值是直接已知的 - 它構建時只是將它傳遞給一個新 Promise

使用我們新的變換 API, 我們現在可以新增支援,將 Data 型別 的 future 轉變為一個 Unboxable 型別(JSON可解碼) 的 future型別,像這樣:

extension Future where Value == Data {
    func unboxed<NextValue: Unboxable>() -> Future<NextValue> {
        return transformed { try unbox(data: $0) }
    }
}複製程式碼

整合所有

現在,我們有了把 UserLoader 升級到支援 Futures & Promises 的所有部分。我將把操作分解為每一行,這樣就更容易看到每一步發生了什麼:

class UserLoader {
    func loadUser(withID id: Int) -> Future<User> {
        let url = apiConfiguration.urlForLoadingUser(withID: id)

        // Request the URL, returning data
        let requestFuture = urlSession.request(url: url)

        // Transform the loaded data into a user
        let unboxedFuture: Future<User> = requestFuture.unboxed()

        // Save the user in the database
        let savedFuture = unboxedFuture.saved(in: database)

        // Return the last future, as it marks the end of the chain
        return savedFuture
    }
}複製程式碼

當然,我們也可以做我們剛開始做的事情,把所有的呼叫串在一起 (這也給我們帶來了利用 Swift 的型別推斷來推斷 User 型別的 future 的好處):

class UserLoader {
    func loadUser(withID id: Int) -> Future<User> {
        let url = apiConfiguration.urlForLoadingUser(withID: id)

        return urlSession.request(url: url)
                         .unboxed()
                         .saved(in: database)
    }
}複製程式碼

結論

在編寫非同步程式碼時,Futures & Promises 是一個非常強大的工具,特別是當您需要將多個操作和轉換組合在一起時。它幾乎使您能夠像同步那樣去編寫非同步程式碼,這可以提高可讀性,並使在需要時可以更容易地移動。

然而,就像大多數抽象化一樣,你本質上是在掩蓋複雜性,把大部分的重舉移到幕後。因此,儘管 urlSession.request(url:) 從外部看,API看起來很好,但除錯和理解到底發生了什麼都會變得更加困難。

我的建議是,如果你在使用 Futures & Promises,那就是讓你的呼叫鏈儘可能精簡。記住,好的文件和可靠的單元測試可以幫助你避免很多麻煩和棘手的除錯。

以下是一些流行的 Swift 版本的 Futures & Promises 開源框架:

你也可以在 GitHub 上找到該篇文章涉及的的所有程式碼。

如果有問題,歡迎留言。我非常希望聽到你的建議!?你可以在下面留言,或者在 Twitter @johnsundell 聯絡我。

另外,你可以獲取最新的 Sundell 的 Swift 播客,我和來自社群的遊客都會在上面回答你關於 Swift 開發的問題。

感謝閱讀 ?。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章