Swift41/90Days - 面向軌道編程 - Swift 中的異常處理

問題

在開發過程當中,異常處理算是比較常見的問題了。git

舉一個比較常見的例子:用戶修改註冊的郵箱,大概分爲如下幾個步驟:github

  • 接收到一個用戶的請求:我要修改郵箱地址
  • 驗證一下請求是否合法,將請求進行格式轉化
  • 更新之前的郵箱地址記錄
  • 給新的郵箱地址發送驗證郵件
  • 將結果返回給用戶

上面的步驟若是一切順利,那代碼確定乾淨利落,可是人生不如意十有八九,上面的步驟很容易出現問題:shell

  • 用戶把郵箱地址填成了家庭地址
  • 用戶是個黑客,沒登陸就發送了更新請求
  • 發送驗證郵件的時候服務器爆炸了,發送郵件失敗

各類異常都會致使此次操做的失敗。編程

方案一

在傳統的處理方案裏,通常是遇到異常就往上拋:swift

這種方案想必你們都不陌生,好比下面這段代碼:vim

NSError *err = nil;
CGFloat result = [MathTool divide:2.5 by:3.0 error:&err];

if (err) {
    NSLog(@"%@", err)
} else {
    [MathTool doSomethingWithResult:result]
}

方案二

而另外一種方案,則是將錯誤的結果繼續日後傳,在最後統一處理:服務器

這種方案有兩個問題:app

  • 在發生異常的時候,如何把異常繼續傳給下面的函數?
  • 當整個流程結束的時候,一個函數如何輸出多個結果?

車軌

咱們把方案二抽象出來,就像是一段車軌:ide

對於同一個輸入,會有 Success 和 Failure 兩種輸出結果,對於 Success 的狀況,咱們但願它能繼續走到後面的流程裏,而對於 Failure 的狀況,它怎麼處理並不重要,咱們但願它能避開後面的流程:函數

因而乎,兩段車軌拼接的時候,便成了這樣:

那麼三段什麼的天然也不在話下了。咱們把下面那根 Failure 的線路擴展一下,便會看到兩條平行的線路,這即是「雙軌模型」 (Two Track Model) ,這是用「面向軌道編程」思想解決異常處理的理論基礎。

這就是 「面向軌道編程」 。一開始我以爲這概念應該只是來搞笑的,仔細想一想彷佛倒也是很貼切。將事件流當作兩條平行的軌道,若是順利則在上行軌道,繼續傳遞給下個業務邏輯去處理,若是出現異常也不慌,直接扔到下行軌道,一直在下行軌道傳遞到終點,在最後統一處理。

這樣處理使得整個流程變成了一條雙進雙出的流水線,有點像是 shell 裏的 pipeline ,上一次的輸出做爲下一次的輸入,十分順暢。並且拼接起來也很方便,咱們能夠把三段拼接成一段暴露給其餘對象使用:

實現

接下來看看在 Swift 中如何應用這種思路處理異常。

首先咱們須要兩種類型的輸出結果:

  • 成功,返回某種類型的值
  • 失敗,返回一個 Error 對象或者失敗的具體信息

照着這個想法,咱們能夠定義一個 Result 枚舉用作輸出:

enum Result<T> {
    case Success(T)
    case Failure(String)
}

利用 Swift 的枚舉特性,咱們能夠在成功的枚舉值裏關聯一些返回值,而後在失敗的狀況下則帶上失敗的消息內容。不過 enum 目前還不支持泛型,咱們能夠在外面封裝一個 Box 類來解決這個問題:

final class Box<T> {
    let value: T
    init(value: T) {
        self.value = value
    }
}

enum Result<T> {
    case Success(Box<T>)
    case Failure(String)
}

再看下一開始咱們舉的那個例子,用這個枚舉類從新寫下就是這樣的:

var result = divide(2.5, by:3)
switch result {
case .Success(let value):
    doSomethingWithResult(value)
case .Failure(let errString):
    println(errString)
}

「看起來好像也沒什麼嘛,你不仍是用了個大括號處理兩種狀況嘛!」(嫌棄臉

確實正如這位熱情的朋友所說,寫完這個例子我也沒以爲有什麼優勢,難道我就是來搞笑的?

「並不。」(嚴肅臉

栗子

接下來咱們舉個栗子玩一玩。爲了更好的觀賞效果,請容許我使用浮誇的寫法和粗暴的命名舉這個栗子。

好比對於即將輸入的數字 x ,咱們但願輸出 4 / (2 / x - 1) 的計算結果。這裏會有兩處出錯的可能,一個是 (2 / x)x 爲 0 ,另外一個就是 (2 / x - 1) 爲 0 的狀況。

先看下傳統寫法:

let errorStr = "輸入錯誤,我很抱歉"
func cal(value: Float) {
    if value == 0 {
        println(errorStr)
    } else {
        let value1 = 2 / value
        let value2 = value1 - 1
        if value2 == 0 {
            println(errorStr)
        } else {
            let value3 = 4 / value2
            println(value3)
        }
    }
}
cal(2)    // 輸入錯誤,我很抱歉
cal(1)    // 4.0
cal(0)    // 輸入錯誤,我很抱歉

那麼用面向軌道的思想怎麼去解決這個問題呢?

大概是這個樣子的:

final class Box<T> {
    let value: T
    init(value: T) {
        self.value = value
    }
}

enum Result<T> {
    case Success(Box<T>)
    case Failure(String)
}

let errorStr = "輸入錯誤,我很抱歉"

func cal(value: Float) {
    func cal1(value: Float) -> Result<Float> {
        if value == 0 {
            return .Failure(errorStr)
        } else {
            return .Success(Box(value: 2 / value))
        }
    }
    func cal2(value: Result<Float>) -> Result<Float> {
        switch value {
        case .Success(let v):
            return .Success(Box(value: v.value - 1))
        case .Failure(let str):
            return .Failure(str)
        }
    }
    func cal3(value: Result<Float>) -> Result<Float> {
        switch value {
        case .Success(let v):
            if v.value == 0 {
                return .Failure(errorStr)
            } else {
                return .Success(Box(value: 4 / v.value))
            }
        case .Failure(let str):
            return .Failure(str)
        }
    }

    let r = cal3(cal2(cal1(value)))
    switch r {
    case .Success(let v):
        println(v.value)
    case .Failure(let s):
        println(s)
    }   
}
cal(2)    // 輸入錯誤,我很抱歉
cal(1)    // 4.0
cal(0)    // 輸入錯誤,我很抱歉

同窗,放下手裏的鍵盤,冷靜下來,有話好好說。

反思

面向軌道以後,代碼量翻了兩倍多,並且~~彷佛~~變得更難讀了。浪費了你們這麼多時間結果就帶來這麼個玩意兒,實在是對不起觀衆們熱情的掌聲。

仔細看下上面的代碼, switch 的操做重複而多餘,都在重複着把 Success 和 Failure 分開的邏輯,實際上每一個函數只須要處理 Success 的狀況。咱們在 Result 中加入 funnel 提早處理掉 Failure 的狀況:

enum Result<T> {
    case Success(Box<T>)
    case Failure(String)

    func funnel<U>(f:T -> Result<U>) -> Result<U> {
        switch self {
        case Success(let value):
            return f(value.value)
        case Failure(let errString):
            return Result<U>.Failure(errString)
        }
    }
}

接下來再回到栗子裏,此時咱們已經再也不須要傳入 Result 值了,只須要傳入 value 便可:

func cal(value: Float) {
    func cal1(v: Float) -> Result<Float> {
        if v == 0 {
            return .Failure(errorStr)
        } else {
            return .Success(Box(2 / v))
        }
    }

    func cal2(v: Float) -> Result<Float> {
        return .Success(Box(v - 1))
    }

    func cal3(v: Float) -> Result<Float> {
        if v == 0 {
            return .Failure(errorStr)
        } else {
            return .Success(Box(4 / v))
        }
    }

    let r = cal1(value).funnel(cal2).funnel(cal3)
    switch r {
    case .Success(let v):
        println(v.value)
    case .Failure(let s):
        println(s)
    }
}

看起來簡潔了一些。咱們能夠經過 cal1(value).funnel(cal2).funnel(cal3) 這樣的鏈式調用來獲取計算結果。

funnel 起到了一個什麼做用呢?它幫咱們把上次的結果進行分流,只將 Success 的軌道對接到了下個業務上,而將 Failure 引到了下一個 Failure 軌道上。也就是說具體的業務只須要處理灰色部分的邏輯:

「面向軌道」編程確實給咱們提供了一個頗有趣的思路。本文只是一個簡單地討論,進一步學習能夠仔細閱讀後面的參考文獻。好比 ValueTransformation.swift 這個真實的完整案例,以及 antitypical/Result 這個封裝完整的 Result 庫。文中的實現方案只是一個比較簡單的方法,和前兩種實現略有差別。

面向鐵軌,春暖花開。願每段代碼都走在 Happy Path 上,願每一個人都有個 Happy Ending 。


參考文獻:

相關文章
相關標籤/搜索