在Swift中進(jìn)行面向協(xié)議的網(wǎng)絡(luò)編程(Protocol-Oriented-Networking in Swift)

一般做法(The Usual Setup)

我們有一個(gè)App,展示來(lái)自世界各地的圖片和信息虽风。當(dāng)然棒口,我們的信息應(yīng)該是從API中抓去的。為了做到這點(diǎn)辜膝,通常我們會(huì)有一個(gè)API請(qǐng)求的對(duì)象无牵。

struct FoodService {
  func get(completionHandler:Result<[Food]> -> Void){
      //make asynchronous API call
      //and return appropriate
  }
}

既然我們做一個(gè)異步的API調(diào)用,我們不能使用swift內(nèi)置的錯(cuò)誤處理返回一個(gè)正確的response或者throw一個(gè)error厂抖。相反茎毁,使用Result enum是一個(gè)很好的實(shí)踐。你可以從這里了解到更多關(guān)于Result enum的信息验游,但是基礎(chǔ)的用法如下:

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

當(dāng)API調(diào)用成功充岛,攜帶有正確承載對(duì)象的Success result被傳入到completion handler中。在FoodService中耕蝉,success result包含foot objectarray崔梗。如果沒(méi)有成功,失敗的result被return垒在,result中會(huì)有一個(gè)失敗發(fā)生時(shí)的error (eg.400)蒜魄。

FoodService的get方法,通常在ViewController中被調(diào)用。ViewController決定如何處理成功或失敗的Result:

//FoodLaLaViewController
var dataSource = [Food]() {
      didSet {
        tableView.reloadData()
    }
}
override func viewDidLoad() {
  super.viewDidLoad()
  getFood()
}

private func getFood() {
  //the get function is called here
  FoodService().get() { [weak self] result in
          switch result {
          case .Success(let food):
                self?.dataSource = Food
          case .Failure(let error):
                self?.showError(error)
      }
   }
}

但是有一個(gè)問(wèn)題....

問(wèn)題(The Problem)

ViewController的getFood () 方法是controller中最重要的方法谈为。畢竟如果API沒(méi)有被正確調(diào)用旅挤,結(jié)果沒(méi)有被正確的處理,controller就不能在屏幕上正確的展示美食伞鲫。為了確保我們的假設(shè)能夠起作用粘茄,為controller寫test就非常重要了。
說(shuō)實(shí)話秕脓,其實(shí)測(cè)試它沒(méi)有那么糟糕....有一個(gè)另辟蹊徑的方法能夠讓你的view controller開(kāi)始測(cè)試柒瓣。
好了,我們準(zhǔn)備開(kāi)始測(cè)試我們的view controller了吠架。

依賴注入(Dependency Injection)

為了測(cè)試ViewController的getFood()方法的正確性芙贫,我們需要注入FoodService到VC中,只需要在function中注入它傍药。

//FoodLaLaViewController
override func viewDidLoad(){
    super.viewDidLoad()
  //passing in a default food service
  getFood(fromService:FoodService())
}

//The FoodService is now injected!
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)
    }
  }
}

下面至少給出了測(cè)試的開(kāi)始:

//FoodLaLaViewControllerTests
func testFetchFood() {
  viewController.getFood(fromService: FoodService())
 // ?? now what?
}

Protocols FTW

下面是我們FoodService的正確版本

struct FoodService {
    func get(completionHandler: Result<[Food]> -> Void) {
        // make asynchronous API call
        // and return appropriate result
    }
}

因?yàn)闇y(cè)試的目的磺平,我們需要能夠重寫get function,為了能夠控制被傳入ViewController的Result拐辽。這樣我們能測(cè)試ViewController對(duì)于成功和失敗的處理拣挪。
因?yàn)镕oodService是一個(gè)結(jié)構(gòu)體,我們不能子類化它薛训。作為替代媒吗,你可以猜中仑氛,我們能夠使用protocols乙埃。
實(shí)際上,你能夠把大部分的方法移動(dòng)到一個(gè)簡(jiǎn)單的protocol中:

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

注意associated type锯岖,這個(gè)協(xié)議將會(huì)為所有的service服務(wù)介袜。在這里我們?cè)贔oodService上使用這個(gè)協(xié)議,但是當(dāng)然你能使用相同的協(xié)議在CakeService或DonutService上出吹。通過(guò)使用generic protocol遇伞,你正添加一個(gè)很棒的格式到你應(yīng)用的所有的service上。
現(xiàn)在捶牢,在FoodService中唯一的變化是鸠珠,Service符合一個(gè)Gettable protocol協(xié)議。就是下面:

struct FoodService: Gettable {
    // [Food] is inferred here as the correct associated type!
    func get(completionHandler: Result<[Food]> -> Void) {
        // make asynchronous API call
        // and return appropriate result
    }
}

使用這中方式另外一個(gè)好處是可讀性。在FoodService里面,你立即可以看到Gettable岸霹,意思很明顯坚芜。你可以用相同的模式來(lái)實(shí)現(xiàn)Creatable、Updatable泌射、Delectable等种呐。你可以立即知道service的作用拂蝎,只要你看到它可缚。

使用協(xié)議(Using the Protocol ??)

所以現(xiàn)在霎迫,是時(shí)候來(lái)重構(gòu)了。在ViewController中帘靡,為了替代 傳FoodService到getFood方法中知给,我們能夠通過(guò)接受一個(gè)Gettable來(lái)約束它。[Food]作為associated type

// 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)在我們可以很簡(jiǎn)單的測(cè)試它描姚!

測(cè)試(Test All the Things!)

為了測(cè)試ViewController的getFood方法炼鞠,我們用Gettable進(jìn)行注入,使用[Food]作為associated type

// FoodLaLaViewControllerTests

class Fake_FoodService: Gettable {
    
    var getWasCalled = false
    // you can assign a failure result here
    // to test that scenario as well
    // the food here is just an array of food for testing purposes
    var result = Result.Success(food)
    
    func get(completionHandler: Result<[Food]> -> Void) {
        getWasCalled = true
        completionHandler(result)
    }
}

所以現(xiàn)在轰胁,我們可以通過(guò)諸如Fake_FoodService來(lái)測(cè)試谒主,ViewController調(diào)用一個(gè)service來(lái)返回[Food] 作為成功的result并作為構(gòu)成tableview的datasource。

// FoodLaLaViewControllerTests

class Fake_FoodService: Gettable {
    var getWasCalled = false
    // you can assign a failure result here
    // to test that scenario as well
    // the food here is just an array of food for testing purposes
    var result = Result.Success(food)
    
    func get(completionHandler: Result<[Food]> -> Void) {
        getWasCalled = true
        completionHandler(result)
    }
}

你現(xiàn)在同樣能為不同的失敗的case寫測(cè)試腳本赃阀。

結(jié)論(Conclusion)

在你的網(wǎng)絡(luò)層使用協(xié)議讓你的code更加通用霎肯、可注入、可測(cè)試榛斯、可讀观游。

原文地址:Protocol-Oriented-Networking in Swift

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市驮俗,隨后出現(xiàn)的幾起案子懂缕,更是在濱河造成了極大的恐慌,老刑警劉巖王凑,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件搪柑,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡索烹,警方通過(guò)查閱死者的電腦和手機(jī)工碾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)百姓,“玉大人渊额,你說(shuō)我怎么就攤上這事±萋#” “怎么了旬迹?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)求类。 經(jīng)常有香客問(wèn)我奔垦,道長(zhǎng),這世上最難降的妖魔是什么仑嗅? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任宴倍,我火速辦了婚禮张症,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鸵贬。我一直安慰自己俗他,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布阔逼。 她就那樣靜靜地躺著兆衅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嗜浮。 梳的紋絲不亂的頭發(fā)上羡亩,一...
    開(kāi)封第一講書(shū)人閱讀 49,730評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音危融,去河邊找鬼畏铆。 笑死,一個(gè)胖子當(dāng)著我的面吹牛吉殃,可吹牛的內(nèi)容都是我干的辞居。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蛋勺,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼瓦灶!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起抱完,我...
    開(kāi)封第一講書(shū)人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤贼陶,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后巧娱,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體碉怔,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年家卖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了眨层。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片庙楚。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡上荡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出馒闷,到底是詐尸還是另有隱情酪捡,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布纳账,位于F島的核電站逛薇,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏疏虫。R本人自食惡果不足惜永罚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一啤呼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧呢袱,春花似錦官扣、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至治专,卻和暖如春卖陵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背张峰。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工泪蔫, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人喘批。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓鸥滨,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親谤祖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子婿滓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容