用 Swift 編寫面向協議的網絡請求

做者:Natasha The Robot,原文連接,原文日期:2016/05/12
譯者:saitjr;校對:Channe;定稿:CMBios

和我一塊兒參加9 月 1 日 - 9月 2 日在紐約舉辦的 Swift 社區慶典?吧!使用優惠碼 NATASHATHEROBOT 能夠得到 $100 的折扣!git

我最近作了個 Swift 面向協議編程實踐(POP?) 的演講。視頻還在處理中。另外一方面,這是演講中 POP 視圖部分的文本記錄,供我和其餘任何人做參考!github

普通的配置方式

假設咱們要作一款展現全球美食圖片和信息的 App。這須要從 API 上拉取數據,那麼,用一個對象來作網絡請求也就是理所固然的了:編程

struct FoodService {
    
    func get(completionHandler: Result<[Food]> -> Void) {
        // 異步網絡請求
        // 返回請求結果
    }
}

一旦咱們建立了異步請求,就不能使用 Swift 內建的錯誤處理來同時返回成功響應和請求錯誤了。不過,卻是給練習 Result 枚舉創造了機會(更多關於 Result 枚舉的信息能夠參考 Error Handling in Swift: Might and Magic),下面是一個最基礎的 Result 寫法:swift

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

當 API 請求成功,回調便會得到 Success 狀態與能正確解析的數據 —— 在當前 FoodService 例子中,成功的狀態包含着美食信息數組。若是請求失敗,會返回 Failure 狀態,幷包含錯誤信息(如 400)。數組

FoodServiceget 方法(發起 API 請求)一般會在 ViewController 中調用,ViewController 來決定請求成功失敗後具體的操做邏輯:網絡

// FoodLaLaViewController
 
var dataSource = [Food]() {
    didSet {
        tableView.reloadData()
    }
}
 
override func viewDidLoad() {
    super.viewDidLoad()
    getFood()
}
 
private func getFood() {
    // 在這裏調用 get() 方法
    FoodService().get() { [weak self] result in
        switch result {
        case .Success(let food):
            self?.dataSource = food
        case .Failure(let error):
            self?.showError(error)
        }
    }
}

但,這樣處理有個問題...異步

有什麼問題

關於 ViewController 中 getFood() 方法的問題是:ViewController 太過依賴這個方法了。若是沒有正確的發起 API 請求或者請求結果(不管 Success 仍是 Failure)沒有正確的處理,那麼界面上就沒有任何數據顯示。ide

爲了確保這個方法沒問題,給它寫測試顯得尤其重要(若是實習生或者你本身之後一不當心改了什麼,那界面上就啥都顯示不出來了)。是的,View Controller Tests ?!測試

說實話,它沒那麼麻煩。這有一個黑魔法來配置 View Controller 測試。

OK,如今已經準備好進行 View Controller 測試了,下一步要作什麼?!

依賴注入

爲了正確地測試 ViewController 中 getFood() 方法,咱們須要注入 FoodService(依賴),而不是直接調用這個方法!

// FoodLaLaViewController
 
override func viewDidLoad() {
    super.viewDidLoad()
 
    // 傳入默認的 food service
    getFood(fromService: FoodService())
}
 
// FoodService 被注入
func getFood(fromService service: FoodService) {
    service.get() { [weak self] result in
        switch result {
        case .Success(let food):
            self?.dataSource = food
        case .Failure(let error):
            self?.showError(error)
        }
    }
}

下面的方法即可開始測試:

// FoodLaLaViewControllerTests
 
func testFetchFood() {
    viewController.getFood(fromService: FoodService())
    
    // ? 接下來?
}

接下來,咱們須要對 FoodService 返回值類型進行更多的約束。

絕殺 —— 協議

目前 FoodService 的結構體是這樣:

struct FoodService {
    
    func get(completionHandler: Result<[Food]> -> Void) {
        // 發起異步請求
        // 返回請求結果
    }
}

爲了方便測試,咱們須要可以重寫 get 方法,來控制哪一個 Result(SuccessFailure)傳給 ViewController,以後就能夠測試 ViewController 是如何處理這兩種結果。

由於 FoodService 是結構體類型,因此不能對其子類化。可是,你猜怎樣,咱們可使用協議來達到重寫目的。

咱們能夠將功能性代碼單獨提到一個協議中:

protocol Gettable {
    associatedtype Data
    
    func get(completionHandler: Result<Data> -> Void)
}

注意這裏標明瞭引用類型(associated type)。這個協議將會用在全部的 service 結構體上,如今咱們只讓 FoodService 去遵循,可是之後還會有 CakeService 或者 DonutService 去遵循。經過使用這個通用性的協議,就能夠在 App 中很是完美的統一全部 service 了。

如今,惟一須要改變的就是 FoodService —— 讓它遵循 Gettable 協議:

struct FoodService: Gettable {
    
    // [Food] 用於限制傳入的引用類型
    func get(completionHandler: Result<[Food]> -> Void) {
        // 發起異步請求
        // 返回請求結果
    }
}

這樣寫還有一個好處 —— 良好的可讀性。看到 FoodService 時,你會馬上注意到 Gettable 協議。你也能夠建立相似的 CreatableUpdatableDelectable,這樣,service 能作的事情顯而易見!

使用協議 ?

是時候重構一下了!在 ViewController 中,相比以前直接調用 FoodServicegetFood 方法,咱們如今能夠將 Gettable 的引用類型限制爲 [Food]

// FoodLaLaViewController
 
override func viewDidLoad() {
    super.viewDidLoad()
    
    getFood(fromService: FoodService())
}
 
func getFood<Service: Gettable where Service.Data == [Food]>(fromService service: Service) {
    service.get() { [weak self] result in
        
        switch result {
        case .Success(let food):
            self?.dataSource = food
        case .Failure(let error):
            self?.showError(error)
        }
    }
}

如今,測試起來容易多了!

測試

要測試 ViewController 的 getFood 方法,咱們須要注入遵循 Gettable 而且引用類型爲 [Food] 的 service:

// FoodLaLaViewControllerTests
 
class Fake_FoodService: Gettable {
    
    var getWasCalled = false
    // 你也能夠在這裏定義一個失敗結果變量,用來測試失敗狀態
    // food 變量是一個數組(在此僅爲測試目的)
    var result = Result.Success(food)
    
    func get(completionHandler: Result<[Food]> -> Void) {
        getWasCalled = true
        completionHandler(result)
    }
}

因此,咱們能夠注入 Fake_FoodService 來測試 ViewController 的確發起了請求,並正確的返回了 [Food] 類型的結果(定義爲 [Food] 是由於 TableView 的 data source 所要用到的類型就是 [Food]):

// FoodLaLaViewControllerTests
 
func testFetchFood_Success() {
    let fakeFoodService = Fake_FoodService()
    viewController.getFood(fromService: fakeFoodService)
    
    XCTAssertTrue(fakeFoodService.getWasCalled)
    XCTAssertEqual(viewController.dataSource.count, food.count)
    XCTAssertEqual(viewController.dataSource, food)
}

如今你也能夠仿照這個寫法完成失敗狀態的測試(好比,根據收到的 ErrorType 顯示對應的錯誤信息)。

總結

使用協議來封裝網絡層,可使代碼統一可注入可測試更可讀

POP 萬歲!

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg

相關文章
相關標籤/搜索