歡迎加入QQ討論群258996829
麥子學(xué)院 頭像
蘋果6袋
6
麥子學(xué)院

如何Swift 編寫面向協(xié)議的網(wǎng)絡(luò)請求?

發(fā)布時間:2016-06-15 00:33  回復(fù):0  查看:2682   最后回復(fù):2016-06-15 00:33  

普通的配置方式

假設(shè)我們要做一款展示全球美食圖片和信息的 App。這需要從 API 上拉取數(shù)據(jù),那么,用一個對象來做網(wǎng)絡(luò)請求也就是理所當(dāng)然的了:

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

一旦我們創(chuàng)建了異步請求,就不能使用 Swift 編程語言(http://www.maiziedu.com/course/16/內(nèi)建的錯誤處理來同時返回成功響應(yīng)和請求錯誤了。不過,倒是給練習(xí) Result 枚舉創(chuàng)造了機(jī)會(更多關(guān)于 Result 枚舉的信息可以參考 Error Handling in Swift: Might and Magic),下面是一個最基礎(chǔ)的 Result 寫法:

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

當(dāng) API 請求成功,回調(diào)便會獲得 Success 狀態(tài)與能正確解析的數(shù)據(jù) —— 在當(dāng)前 FoodService 例子中,成功的狀態(tài)包含著美食信息數(shù)組。如果請求失敗,會返回 Failure 狀態(tài),并包含錯誤信息(如 400)。

FoodService  get 方法(發(fā)起 API 請求)通常會在 ViewController 中調(diào)用,ViewController 來決定請求成功失敗后具體的操作邏輯:

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

但,這樣處理有個問題…

有什么問題

關(guān)于 ViewController  getFood() 方法的問題是:ViewController 太過依賴這個方法了。如果沒有正確的發(fā)起 API 請求或者請求結(jié)果(無論 Success還是 Failure)沒有正確的處理,那么界面上就沒有任何數(shù)據(jù)顯示。

為了確保這個方法沒問題,給它寫測試顯得尤為重要(如果實(shí)習(xí)生或者你自己以后一不小心改了什么,那界面上就啥都顯示不出來了)。是的,View Controller Tests

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

OK,現(xiàn)在已經(jīng)準(zhǔn)備好進(jìn)行 View Controller 測試了,下一步要做什么?!

依賴注入

為了正確地測試 ViewController  getFood() 方法,我們需要注入 FoodService(依賴),而不是直接調(diào)用這個方法!

// FoodLaLaViewController
 
override func viewDidLoad() {
    super.viewDidLoad()
 
    // 傳入默認(rèn)的 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 返回值類型進(jìn)行更多的約束。

絕殺 —— 協(xié)議

目前 FoodService 的結(jié)構(gòu)體是這樣:

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

為了方便測試,我們需要能夠重寫 get 方法,來控制哪個 ResultSuccess  Failure)傳給 ViewController,之后就可以測試 ViewController 是如何處理這兩種結(jié)果。

因?yàn)?span> FoodService 是結(jié)構(gòu)體類型,所以不能對其子類化。但是,你猜怎樣,我們可以使用協(xié)議來達(dá)到重寫目的。

我們可以將功能性代碼單獨(dú)提到一個協(xié)議中:

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

注意這里標(biāo)明了引用類型(associated type)。這個協(xié)議將會用在所有的 service 結(jié)構(gòu)體上,現(xiàn)在我們只讓 FoodService 去遵循,但是以后還會有CakeService 或者 DonutService 去遵循。通過使用這個通用性的協(xié)議,就可以在 App 中非常完美的統(tǒng)一所有 service 了。

現(xiàn)在,唯一需要改變的就是 FoodService —— 讓它遵循 Gettable 協(xié)議:

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

這樣寫還有一個好處 —— 良好的可讀性??吹?span> FoodService 時,你會立刻注意到 Gettable 協(xié)議。你也可以創(chuàng)建類似的Creatable,Updatable,Delectable,這樣,service 能做的事情顯而易見!

使用協(xié)議

是時候重構(gòu)一下了!在 ViewController 中,相比之前直接調(diào)用 FoodService  getFood 方法,我們現(xiàn)在可以將 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)
        }
    }
}

現(xiàn)在,測試起來容易多了!

測試

要測試 ViewController  getFood 方法,我們需要注入遵循 Gettable 并且引用類型為 [Food] service

// FoodLaLaViewControllerTests
 
class Fake_FoodService: Gettable {
    
    var getWasCalled = false
    // 你也可以在這里定義一個失敗結(jié)果變量,用來測試失敗狀態(tài)
    // food 變量是一個數(shù)組(在此僅為測試目的)
    var result = Result.Success(food)
    
    func get(completionHandler: Result<[Food]> -> Void) {
        getWasCalled = true
        completionHandler(result)
    }
}

所以,我們可以注入 Fake_FoodService 來測試 ViewController 的確發(fā)起了請求,并正確的返回了 [Food] 類型的結(jié)果(定義為 [Food] 是因?yàn)?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)
}

現(xiàn)在你也可以仿照這個寫法完成失敗狀態(tài)的測試(比如,根據(jù)收到的 ErrorType 顯示對應(yīng)的錯誤信息)。

總結(jié)

使用協(xié)議來封裝網(wǎng)絡(luò)層,可以使代碼統(tǒng)一、 可注入、 可測試、更可讀。

您還未登錄,請先登錄

熱門帖子

最新帖子

?