做者: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)。數組
FoodService
的 get
方法(發起 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(Success
或 Failure
)傳給 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
協議。你也能夠建立相似的 Creatable
,Updatable
,Delectable
,這樣,service 能作的事情顯而易見!
是時候重構一下了!在 ViewController 中,相比以前直接調用 FoodService
的 getFood
方法,咱們如今能夠將 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。