使用泛型與函數(shù)式思想高效解析 JSON

更多優(yōu)秀譯文請關注我們的微信公眾號:learnSwift

原文連接:Efficient JSON in Swift with Functional Concepts and Generics

就在幾個月前,蘋果推出了一門全新的編程語言,其名為Swift, 這讓我們對未來 iOS 和 OS X 開發(fā)充滿了期待與興奮。人們紛紛開始使用 Xcode Beta1 版本來進行 Swift 開發(fā),但是很快就發(fā)現(xiàn)解析 JSON 這一常見的操作在 Swift 中并不如在 Objectitve-C 中那樣快捷和方便。Swift 是一門靜態(tài)類型的語言,這意味我們不能簡單地將對象賦值給一個特定類型的變量,并且讓編譯器相信這些對象就是我們所聲明的那種類型。在 Swift 當中,編譯器會進行檢查,以確保我們不會意外地觸發(fā)運行時錯誤。這使得我們可以依賴編譯器來寫出一些無 bug 的代碼,同時我們必須做許多額外的工作來使編譯器不報錯。在這篇文章當中,我將使用函數(shù)式思想和泛型來探討如何編寫易讀高效的 JSON 解析代碼。

請求用戶(User)模型

我們要做的事就是將網(wǎng)絡請求獲得的數(shù)據(jù)解析成 JSON。之前我們一直使用的是 NSJSONSerialization.JSONObjectWithData(NSData, Int, &NSError)方法,這個方法返回一個可選的 JSON 數(shù)據(jù)類型,如果解析過程出錯會得到 NSError 類型的數(shù)據(jù)。在 Objective-C 當中,JSON 的數(shù)據(jù)類型是一個可以包含任何其它數(shù)據(jù)類型的 NSDictionary類型。 而在 Swift 當中, 新的字典類型要求我們必須顯式指定它所包含的數(shù)據(jù)的類型。JSON 數(shù)據(jù)被指定為Dictionary<String, AnyObject>類型。這里使用 AnyObject的原因是 JSON 的值有可能為 StringDouble、 Bool、 Array、 Dictionary 或者 null。當我們使用 JSON 來生成模型數(shù)據(jù)時,必須對每一個從 JSON 字典中獲取到的值進行判斷,以確保這個值與我們模型中屬性的類型一致。

下面我們來看一個用戶(user)的模型:

struct User {
  let id: Int
  let name: String
  let email: String
}

然后,來看一下對當前用戶的請求和響應代碼:

func getUser(request: NSURLRequest, callback: (User) -> ()) {
  let task = NSURLSession.sharedSession().dataTaskWithRequest(request)
  { data, urlResponse, error in
    var jsonErrorOptional: NSError?
    let jsonOptional: AnyObject! = 
    NSJSONSerialization.JSONObjectWithData(data, 
    options: NSJSONReadingOptions(0), error: &jsonErrorOptional)
    if let json = jsonOptional as? Dictionary<String, AnyObject> {
      if let id = json["id"] as AnyObject? as? Int { 
      // 在 beta5 中,存在一個 bug,所以我們首先要強行轉換成 AnyObject?
        if let name = json["name"] as AnyObject? as? String {
          if let email = json["email"] as AnyObject? as? String {
            let user = User(id: id, name: name, email: email)
            callback(user)
          }
        }
      }
    }
  }
  task.resume()
}

在一長串的if-let語句之后,我們終于拿到User對象。可以想象一下,如果一個模型的屬性很多,這些代碼會有多丑。并且,這里我們沒有進行錯誤處理,這意味著,只要其中一步出錯我們就獲取不到任何數(shù)據(jù)。最后并且最重要的一點是,我們必須對每個需要從網(wǎng)絡 API 中獲取的模型寫一遍類似上面這樣的代碼,這將會導致很多重復代碼。

在對代碼進行重構之前,讓我們先對JSON的幾種類型定義別名,以使之后的代碼看起來更簡潔。

typealias JSON = AnyObject
typealias JSONDictionary = Dictionary<String, JSON>
typealias JSONArray = Array<JSON>

重構:添加錯誤處理

首先,我們將通過學習第一個函數(shù)式編程的概念,Either<A, B>類型,來對代碼進行重構,以使其能進行錯誤處理。這可以使代碼在正確的情況下返回用戶對象,而在出錯時返回一個錯誤對象。在 Swift 當中可以使用如下方法來實現(xiàn) Either<A, B>

enum Either<A, B> {
  case Left(A)
  case Right(B)
}

我們可以使用 Either<NSError, User> 作為傳入回調的參數(shù),這樣調用者便可以直接處理解析過的User對象或者錯誤。

func getUser(request: NSURLRequest, callback: 
                (Either<NSError, User>) -> ()) {
  let task = NSURLSession.sharedSession().dataTaskWithRequest(request) 
  { data, urlResponse, error in
    // 如果響應返回錯誤,我們將把錯誤發(fā)送給回調
    if let err = error {
      callback(.Left(err))
      return
    }
    
    var jsonErrorOptional: NSError?
    let jsonOptional: JSON! = 
    NSJSONSerialization.JSONObjectWithData(data, 
    options: NSJSONReadingOptions(0), error: &jsonErrorOptional)
    
    // 如果我們不能解析 JSON,就將發(fā)送回去一個錯誤
    if let err = jsonErrorOptional {
      callback(.Left(err))
      return
    }
    
    if let json = jsonOptional as? JSONDictionary {
      if let id = json["id"] as AnyObject? as? Int {
        if let name = json["name"] as AnyObject? as? String {
          if let email = json["email"] as AnyObject? as? String {
            let user = User(id: id, name: name, email: email)
            callback(.Right(user))
            return
          }
        }
      }
    }

    // 如果我們不能解析所有的屬性,就將發(fā)送回去一個錯誤
    callback(.Left(NSError()))
  }
  task.resume()
}

現(xiàn)在調用getUser的地方可以直接使用Either,然后對接收到的用戶對象進行處理,或者直接顯示錯誤。

getUser(request) { either in
  switch either {
  case let .Left(error):
    //顯示錯誤信息

  case let .Right(user):
    //對user進行操作
  }
}

我們假設Left一直是NSError,這可以進一步簡化代碼。我們可以使用一個不同的類型 Result<A> 來保存我們需要的類型數(shù)據(jù)和錯誤信息。它的實現(xiàn)方式如下:

enum Result<A> {
  case Error(NSError)
  case Value(A)
}

在當前的 Swift 版本(Beta 5)中,上面的 Result類型會造成編譯錯誤(譯者注:事實上,在 Swift 1.2 中還是有錯誤)。 Swift 需要知道存儲在enum當中數(shù)據(jù)的確切類型??梢酝ㄟ^創(chuàng)建一個靜態(tài)類作為包裝類型來解決這個問題:

final class Box<A> {
  let value: A

  init(_ value: A) {
    self.value = value
  }
}

enum Result<A> {
  case Error(NSError)
  case Value(Box<A>)
}

Either 替換為 Result,代碼將變成這樣:

func getUser(request: NSURLRequest, callback: (Result<User>) -> ()) {
  let task = NSURLSession.sharedSession().dataTaskWithRequest(request) 
  { data, urlResponse, error in
    // 如果響應返回錯誤,我們將把錯誤發(fā)送給回調
    if let err = error {
      callback(.Error(err))
      return
    }

    var jsonErrorOptional: NSError?
    let jsonOptional: JSON! =
     NSJSONSerialization.JSONObjectWithData(data, 
     options: NSJSONReadingOptions(0), error: &jsonErrorOptional)
    
    // 如果我們不能解析 JSON,就返回一個錯誤
    if let err = jsonErrorOptional {
      callback(.Error(err))
      return
    }

    if let json = jsonOptional as? JSONDictionary {
      if let id = json["id"] as AnyObject? as? Int {
        if let name = json["name"] as AnyObject? as? String {
          if let email = json["email"] as AnyObject? as? String {
            let user = User(id: id, name: name, email: email)
            callback(.Value(Box(user)))
            return
          }
        }
      }
    }

    // 如果我們不能解析所有的屬性,就返回一個錯誤
    callback(.Error(NSError()))
  }
  task.resume()
}
getUser(request) { result in
  switch result {
  case let .Error(error):
    // 顯示錯誤信息

  case let .Value(boxedUser):
    let user = boxedUser.value
    // 對 user 繼續(xù)操作
  }
}

改變不是很大,我們繼續(xù)努力。

重構: 消除多層嵌套

接下來,我們將為每個不同的類型創(chuàng)建一個 JSON 解析器來消滅掉那些丑陋的解析 JSON 的代碼。在這個對象中我們只用到了 String, IntDictionary 三種類型,所以我們需要三個函數(shù)來對這三種類型進行解析。

func JSONString(object: JSON?) -> String? {
  return object as? String
}

func JSONInt(object: JSON?) -> Int? {
  return object as? Int
}

func JSONObject(object: JSON?) -> JSONDictionary? {
  return object as? JSONDictionary
}

現(xiàn)在,解析 JSON 的代碼看起來應該是這樣的:

if let json = JSONObject(jsonOptional) {
  if let id = JSONInt(json["id"]) {
    if let name = JSONString(json["name"]) {
      if let email = JSONString(json["email"]) {
        let user = User(id: id, name: name, email: email)
      }
    }
  }
}

即使使用了這些函數(shù),還是需要用到一大堆的 if-let 語句。函數(shù)式編程中的 Monads,Applicative Functors,以及 Currying 概念可以幫助我們來壓縮這段代碼。首先看看與 Swift 中的可選類型十分相似的 Monad。Monad 中有一個綁定(bind)運行符,這個運行符可以給一個可選類型綁定一個函數(shù),這個函數(shù)接受一個非可選類型參數(shù),并返回一個可選類型的返回值。如果第一個可選類型是 .None這個運行符會返回 .None ,否則它會對這個可選類型進行解包,并使用綁定的函數(shù)調用解包后的數(shù)據(jù)。

infix operator >>> { associativity left precedence 150 }

func >>><A, B>(a: A?, f: A -> B?) -> B? {
  if let x = a {
    return f(x)
  } else {
    return .None
  }
}

在其它的函數(shù)式語言中,都是使用 >>= 來作為綁定(bind)運算符,但是在 Swift 中這個運算符被用于二進制位的移位操作,所以我們使用了 >>> 來作為替代。在 JSON 代碼中使用這個操作符可以得到如下代碼:

if let json = jsonOptional >>> JSONObject {
  if let id = json["id"] >>> JSONInt {
    if let name = json["name"] >>> JSONString {
      if let email = json["email"] >>> JSONString {
        let user = User(id: id, name: name, email: email)
      }
    }
  }
}

接著就可以去掉解析函數(shù)里的可選參數(shù):

func JSONString(object: JSON) -> String? {
  return object as? String
}

func JSONInt(object: JSON) -> Int? {
  return object as? Int
}

func JSONObject(object: JSON) -> JSONDictionary? {
  return object as? JSONDictionary
}

Functors 有一個fmap運算符,可以在某些上下文中通過函數(shù)應用到解包后的值上面。Applicative Functors 也有apply運算符,可以在某些上下文中通過解包后的函數(shù)應用到解包后的值上面。這里的上下文是一個包含了值的可選值。這就意味著我們可以使用一個能夠帶有多個非可選值的函數(shù)來連接多個可選值。如果所有的值都存在,.Some會得到可選值解包的結果。如果其中任何值是.None,我們將得到.None??梢栽?Swift 中像下面這樣定義這些運算符:

infix operator <^> { associativity left } // Functor's fmap (usually <$>)
infix operator <*> { associativity left } // Applicative's apply

func <^><A, B>(f: A -> B, a: A?) -> B? {
  if let x = a {
    return f(x)
  } else {
    return .None
  }
}

func <*><A, B>(f: (A -> B)?, a: A?) -> B? {
  if let x = a {
    if let fx = f {
      return fx(x)
    }
  }
  return .None
}

先別著急使用這些代碼,由于 Swift 不支持自動柯里化(auto-currying), 我們需要手動柯里化(curry)結構體User中的init方法。柯里化的意思是當我們給定一個函數(shù)的參數(shù)比它原來的參數(shù)更少時,這個函數(shù)將返回一個包含剩余參數(shù)的函數(shù)。我們的User模型將看起來像這樣:

struct User {
  let id: Int
  let name: String
  let email: String

  static func create(id: Int)(name: String)(email: String) -> User {
    return User(id: id, name: name, email: email)
  }
}

把以上代碼合并到一起,我們的 JSON 解析現(xiàn)在看起來是這樣的:


if let json = jsonOptional >>> JSONObject {
  let user = User.create <^>
              json["id"]    >>> JSONInt    <*>
              json["name"]  >>> JSONString <*>
              json["email"] >>> JSONString
}

如果我們解析器的任何部分返回.None,那么user就會是.None。這看起來已經(jīng)好多了,但是我們還沒有優(yōu)化完畢。

到目前為止,我們的getUser函數(shù)看起來像這樣:

func getUser(request: NSURLRequest, callback: (Result<User>) -> ()) {
  let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, urlResponse, error in
    // 如果響應返回錯誤,返回錯誤
    if let err = error {
      callback(.Error(err))
      return
    }

    var jsonErrorOptional: NSError?
    let jsonOptional: JSON! = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(0), error: &jsonErrorOptional)

    // 如果我們不能解析 JSON,返回錯誤
    if let err = jsonErrorOptional {
      callback(.Error(err))
      return
    }

    if let json = jsonOptional >>> JSONObject {
      let user = User.create <^>
                  json["id"]    >>> JSONInt    <*>
                  json["name"]  >>> JSONString <*>
                  json["email"] >>> JSONString
      if let u = user {
        callback(.Value(Box(u)))
        return
      }
    }

    // 如果我們不能解析所有的屬性,就返回錯誤
    callback(.Error(NSError()))
  }
  task.resume()
}

重構:通過綁定消除多個返回

觀察到在上面的函數(shù)中,我們的調用了callback函數(shù) 4 次。漏掉任何一次都會制造 bug。我們可以把這個函數(shù)分解成 3 個互不相關的部分,從而消除潛在的 bug 并重構這個函數(shù)。這三個部分是:解析響應,解析數(shù)據(jù)為 JSON 和解析 JSON 為User對象。這些步驟中的每一步都帶有一個輸入和返回下一個步驟的輸入或者錯誤。綁定我們的Result類型看起來是一個不錯的方案。
parseResponse函數(shù)需要Result數(shù)據(jù)和響應的狀態(tài)碼。iOS API 只提供了NSURLResponse并保證數(shù)據(jù)獨立。所以我們創(chuàng)建一個小結構體來輔助一下:

struct Response {
  let data: NSData
  let statusCode: Int = 500

  init(data: NSData, urlResponse: NSURLResponse) {
    self.data = data
    if let httpResponse = urlResponse as? NSHTTPURLResponse {
      statusCode = httpResponse.statusCode
    }
  }
}

現(xiàn)在我們可以把Response結構體傳入parseResponse函數(shù),然后在處理數(shù)據(jù)之前處理錯誤。

func parseResponse(response: Response) -> Result<NSData> {
  let successRange = 200..<300
  if !contains(successRange, response.statusCode) {
    return .Error(NSError()) // 自定義你想要的錯誤信息
  }
  return .Value(Box(response.data))
}

下一個函數(shù)需要我們將一個可選值轉換成Result類型,我們先來抽象一下。

func resultFromOptional<A>(optional: A?, error: NSError) -> Result<A> {
  if let a = optional {
    return .Value(Box(a))
  } else {
    return .Error(error)
  }
}

接下來的函數(shù)需要解析數(shù)據(jù)為 JSON:

func decodeJSON(data: NSData) -> Result<JSON> {
  let jsonOptional: JSON! = 
  NSJSONSerialization.JSONObjectWithData(data, 
  options: NSJSONReadingOptions(0), error: &jsonErrorOptional)
  return resultFromOptional(jsonOptional, NSError()) 
  // 使用默認的錯誤或者自定義錯誤信息
}

然后,我們在User類型中添加 JSON 到User類型的轉換:

struct User {
  let id: Int
  let name: String
  let email: String

  static func create(id: Int)(name: String)(email: String) -> User {
    return User(id: id, name: name, email: email)
  }

  static func decode(json: JSON) -> Result<User> {
    let user = JSONObject(json) >>> { dict in
      User.create <^>
          dict["id"]    >>> JSONInt    <*>
          dict["name"]  >>> JSONString <*>
          dict["email"] >>> JSONString
    }
    return resultFromOptional(user, NSError()) // 自定義錯誤消息
  }
}

合并代碼之前,需要擴展一下綁定, 讓>>>來配合Result類型:

func >>><A, B>(a: Result<A>, f: A -> Result<B>) -> Result<B> {
  switch a {
  case let .Value(x):     return f(x.value)
  case let .Error(error): return .Error(error)
  }
}

然后我們添加一個Result的自定義構造器:

enum Result<A> {
  case Error(NSError)
  case Value(Box<A>)

  init(_ error: NSError?, _ value: A) {
    if let err = error {
      self = .Error(err)
    } else {
      self = .Value(Box(value))
    }
  }
}

現(xiàn)在我們可以把所有的函數(shù)使用綁定運算符連接到一起了:

func getUser(request: NSURLRequest, callback: (Result<User>) -> ()) {
  let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, urlResponse, error in
    let responseResult = Result(error, 
    Response(data: data, urlResponse: urlResponse))
    let result = responseResult >>> parseResponse
                                >>> decodeJSON
                                >>> User.decode
    callback(result)
  }
  task.resume()
}

Wow,即使再次書寫這些代碼,我都對這些結果感到興奮。你可能會想,"這已經(jīng)非??犰帕耍覀円呀?jīng)迫不及待的想用它了!",但是這還不算完!

重構:使用泛型抽象類型

已經(jīng)非常棒了,但是我們仍然想編寫這個解析器適用于任何類型。我可以使用泛型(Generics)來使得解析器完全抽象。

我們引入JSONDecodable協(xié)議,讓上面的類型遵守它。協(xié)議看起來是這樣的:

protocol JSONDecodable {
  class func decode(json: JSON) -> Self?
}

然后,我們編寫一個函數(shù),解析任何遵守JSONDecodable協(xié)議的類型為Result類型:

func decodeObject<A: JSONDecodable>(json: JSON) -> Result<A> {
  return resultFromOptional(A.decode(json), NSError()) // 自定義錯誤
}

現(xiàn)在我們可以讓User遵守協(xié)議:

struct User: JSONDecodable {
  let id: Int
  let name: String
  let email: String

  static func create(id: Int)(name: String)(email: String) -> User {
    return User(id: id, name: name, email: email)
  }

  static func decode(json: JSON) -> User? {
    return JSONObject(json) >>> { d in
      User.create <^>
        d["id"]    >>> JSONInt    <*>
        d["name"]  >>> JSONString <*>
        d["email"] >>> JSONString
  }
}

我們改變了User的解析函數(shù),用可選的User替換掉Result<User>。這樣我們就擁有了一個抽象的函數(shù),可以在解碼后調用resultFromOptional,替代之前模型中必須使用的decode函數(shù)。

最后,我們抽象performRequest函數(shù)中的解析和解碼過程,讓它們變得更加易讀。下面是最終的performRequestparseResult函數(shù):

func performRequest<A: JSONDecodable>(request: NSURLRequest, callback: (Result<A>) -> ()) {
  let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, urlResponse, error in
    callback(parseResult(data, urlResponse, error))
  }
  task.resume()
}

func parseResult<A: JSONDecodable>(data: NSData!, urlResponse: NSURLResponse!, error: NSError!) -> Result<A> {
  let responseResult = Result(error, Response(data: data, urlResponse: urlResponse))
  return responseResult >>> parseResponse
                        >>> decodeJSON
                        >>> decodeObject
}

繼續(xù)學習

實例代碼放在了GitHub上供下載
如果你對函數(shù)式編程或者這篇文章討論的任何概念感興趣,請查閱Haskell編程語言和Learn You a Haskell書中的一篇特定文章,同時,請查閱Pat Brisbin寫的博客:Applicative Options Parsing in Haskell

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 發(fā)現(xiàn) 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 14,996評論 4 61
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,500評論 19 139
  • 生活就像一塊巧克力,你永遠不知道它到底有多黑。 生活如一片死水,她出生了,上學了,被欺負了,又在眾多否定的聲音中考...
    優(yōu)票票閱讀 147評論 0 0
  • 驀然回首三十載, 一事無成心自憐。 雄心猶在已無志, 兀自空想花已殘。 人生匆忙至而立,猛然回首事無成; 呆坐空想...
    閑居散人閱讀 921評論 0 5
  • 風起龍飛閱讀 263評論 0 13

友情鏈接更多精彩內容