基于 Swift 的面向協(xié)議編程
當(dāng) Swift 剛剛出現(xiàn)的時(shí)候,學(xué)習(xí)新東西都是令人興奮的。第一年,我很高興能學(xué)習(xí)它,我之前在 Swift 里面使用我的 Objective C 代碼 (有的時(shí)候用些值類型和更加有趣的東西)。但是直到去年的 WWDC,協(xié)議擴(kuò)展出現(xiàn)了。
Dave Abrahams (讓你大開眼界的教授) 做了一次令人大開眼界的演講 “基于 Swift 的面向協(xié)議編程”。他聲稱 “Swift 就是一個(gè)面向協(xié)議的編程語言。” 如果你看看 Swift 的標(biāo)準(zhǔn)庫(kù),那有超過 50 個(gè)協(xié)議。這就是這門語言的成形之處,它使用了許多的協(xié)議而且這也是我們想借鑒的地方。Dave 還給了一個(gè)如何使用協(xié)議來改進(jìn)我們現(xiàn)有代碼的例子。他使用了 drawables 的例子,比如正方形、三角形,圓形。使用協(xié)議能夠讓它們的實(shí)現(xiàn)變得特別令人吃驚。我是被震撼到了,但是對(duì)于我來說我卻無法直接使用,因?yàn)槲以诿刻斓墓ぷ髦胁皇褂?drawables。
回去以后,我冥思苦想,我該如何在每天的程序中使用面向協(xié)議編程呢。我們都有些從 Objective-C 和其他編程語言繼承下來的編程模式,所以從面向?qū)ο筠D(zhuǎn)變到面向協(xié)議是一件很難的事情。
實(shí)踐 POP!
過去一年,我終于有機(jī)會(huì)實(shí)驗(yàn)一下使用協(xié)議,我想分享些我改進(jìn)代碼的例子。因?yàn)檫@是實(shí)踐面向協(xié)議編程,我將會(huì)講到 View、 (UITable)ViewController 和 Networking。希望這能幫助你們考慮如何在你們的實(shí)際工作中使用協(xié)議。
Views
讓我們假設(shè)你的產(chǎn)品經(jīng)理過來和你說,“我們想在點(diǎn)擊那個(gè)按鈕時(shí)候出現(xiàn)一個(gè)視圖,而且它會(huì)抖動(dòng)。” 這是一個(gè)非常常見的動(dòng)畫,比如,在你的密碼輸入框上 – 當(dāng)用戶輸入???錯(cuò)誤密碼時(shí),它就會(huì)抖動(dòng)。
我們常常都是從 Stack Overflow 開始的(笑)。一些人可能已經(jīng)有了 Swift 抖動(dòng)對(duì)象的基礎(chǔ)代碼。一些人甚至都有 Swift 的抖動(dòng)對(duì)象的代碼,我想都不用想,只要稍稍修改一下。最難的部分當(dāng)然是架構(gòu):我在哪里集成這些代碼呢?
// FoodImageView.swift
import UIKit
class FoodImageView: UIImageView {
func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
layer.addAnimation(animation, forKey: "position")
}
}
我將創(chuàng)建一個(gè) UIImageView
的子類,創(chuàng)建我的 FoodImageView
然后增加一個(gè)抖動(dòng)的動(dòng)畫:
// ViewController.swift
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var foodImageView: FoodImageView!
@IBAction func onShakeButtonTap(sender: AnyObject) {
foodImageView.shake()
}
}
在我的 view controller 里面,在 interface builder 里我連接我的 view,把它做為 FoodImageView
的子類,我有一個(gè) shake 函數(shù),然后 完成了!。10 分鐘我就完成了這個(gè)功能。我很開心,我的代碼工作得很正常。
然后,你的產(chǎn)品經(jīng)理過來說,”你需要在抖動(dòng)視圖的時(shí)候抖動(dòng)按鈕。” 然后我回去對(duì)按鈕做了同樣的事情。
// ShakeableButton.swift
import UIKit
class ActionButton: UIButton {
func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
layer.addAnimation(animation, forKey: "position")
}
}
子類,創(chuàng)建一個(gè)按鈕,增加一個(gè) shake()
函數(shù),和我的 ViewController
。現(xiàn)在我能抖動(dòng)我的 food 圖像視圖和按鈕了,完成了。
// ViewController.swift
class ViewController: UIViewController {
@IBOutlet weak var foodImageView: FoodImageView!
@IBOutlet weak var actionButton: ActionButton!
@IBAction func onShakeButtonTap(sender: AnyObject) {
foodImageView.shake()
actionButton.shake()
}
}
幸運(yùn)的是,這會(huì)給你一個(gè)警告:我在兩個(gè)地方重復(fù)了抖動(dòng)的代碼。如果我想改變抖動(dòng)的幅度,我需要改兩處代碼,這很不好。
// UIViewExtension.swift
import UIKit
extension UIView {
func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
layer.addAnimation(animation, forKey: "position")
}
}
作為一個(gè)優(yōu)秀的程序員,我們馬上會(huì)意識(shí)到這點(diǎn),而且試圖重構(gòu)。如果你以前使用過 Objective-C,我會(huì)創(chuàng)建一個(gè) UIView
的類別,在 Swift 里面,這就是擴(kuò)展。
我能這樣做,因?yàn)?UIButton
和 UIImageView
都是 UI 視圖。我能擴(kuò)展 UI 視圖而且增加一個(gè) shake 函數(shù)。現(xiàn)在我仍然可以給我的按鈕和圖像視圖都加上其他的邏輯,但是 shake 函數(shù)就到處都是了。
class FoodImageView: UIImageView {
// other customization here
}
class ActionButton: UIButton {
// other customization here
}
class ViewController: UIViewController {
@IBOutlet weak var foodImageView: FoodImageView!
@IBOutlet weak var actionButton: ActionButton!
@IBAction func onShakeButtonTap(sender: AnyObject) {
foodImageView.shake()
actionButton.shake()
}
}
馬上我們就能發(fā)現(xiàn)可讀性很差了。例如,對(duì)于 foodImageView
和 actionButton
來說,你看不出來任何抖動(dòng)的意圖。整個(gè)類里面沒有任何東西能告訴你它需要抖動(dòng)。這不清楚,是因?yàn)閯e處會(huì)隨機(jī)存在一個(gè)抖動(dòng)函數(shù)。
如果你常常為類別和 UI view 的擴(kuò)展這樣做的話,你可能會(huì)有更好的辦法。這就是所謂的 科學(xué)怪人的垃圾地點(diǎn),你增加了一個(gè) shake 函數(shù)然后有人來和你說, “我想要一個(gè)可調(diào)暗的視圖”。然后你增加一個(gè) dim 函數(shù)和其他別處隨機(jī)的調(diào)用函數(shù)。這樣,文件會(huì)變得越來越長(zhǎng),不可讀,很難找到垃圾,因?yàn)檫@些隨機(jī)調(diào)用的事情都可以在 UI 視圖里面完成,盡管有些時(shí)候也許只有一兩個(gè)地方需要這么做。
意圖是什么并不清晰。我們?nèi)绾胃淖冞@點(diǎn)呢?
這是一次面向協(xié)議編程的演講,我們當(dāng)然會(huì)用到協(xié)議。讓我們創(chuàng)建一個(gè) Shakeable
的協(xié)議:
// Shakeable.swift
import UIKit
protocol Shakeable { }
extension Shakeable where Self: UIView {
func shake() {
// implementation code
}
}
在協(xié)議擴(kuò)展的幫助下,你可以把它們限制在一個(gè)特定的類里面。在這個(gè)例子里面,我能抽出我的 shake 函數(shù),然后用類別,我能說這是我們需要遵循的唯一的東西,只有 UI 視圖會(huì)有這個(gè)函數(shù)。
你仍然可以使用你原來想用的同樣強(qiáng)大的擴(kuò)展功能,但是你有協(xié)議了。任何遵循協(xié)議的非視圖不會(huì)工作。只有視圖才能有這個(gè) shake 的默認(rèn)實(shí)現(xiàn)。
class FoodImageView: UIImageView, Shakeable {
}
class ActionButton: UIButton, Shakeable {
}
我們可以看到 FoodImageView
和 ActionButton
會(huì)遵循 Shakeable
協(xié)議。它們會(huì)有 shake
函數(shù),現(xiàn)在的可讀性強(qiáng)多了 –- 我可以理解 shaking 是有意存在的。如果你在別處使用視圖,我需要想想,”在這也需要抖動(dòng)嗎?”。它增強(qiáng)了可讀性,但是代碼還是閉合的和可重用的。
假設(shè)我們想抖動(dòng)和調(diào)暗視圖。我們會(huì)有另外一個(gè)協(xié)議,一個(gè) Dimmable
協(xié)議,然后我們可以為了調(diào)暗做一個(gè)協(xié)議擴(kuò)展。再?gòu)?qiáng)調(diào)一遍,通過看類的定義來知曉這個(gè)類的用途,這樣,意圖就會(huì)很明顯了。
class FoodImageView: UIImageView, Shakeable, Dimmable {
}
關(guān)于重構(gòu),當(dāng)我們說 “我不想要抖動(dòng)了”的時(shí)候,你只需要?jiǎng)h除相關(guān)的 Shakeable
協(xié)議就好了。
class FoodImageView: UIImageView, Dimmable {
}
現(xiàn)在它只能調(diào)暗了。插入是非常容易的,通過使用協(xié)議我們很容易獲得樂高似的架構(gòu)。看看 這篇文章 如果你想學(xué)習(xí)使用協(xié)議的其他更強(qiáng)大的方式的話,試試創(chuàng)建一個(gè)有過渡效果的可調(diào)暗的視圖。
現(xiàn)在我們高興了,可以去吃 Pop-tarts 了。
(UITable)ViewControllers
這是一個(gè)應(yīng)用,Instagram 食物:它給你展示不同地點(diǎn)的美食照片。
// FoodLaLaViewController
override func viewDidLoad() {
super.viewDidLoad()
let foodCellNib = UINib(NibName: "FoodTableViewCell", bundle: nil)
tableView.registerNib(foodCellNib,
forCellReuseIdentifier: "FoodTableViewCell")
}
這是一個(gè) tableView
。這是我們一直都會(huì)編寫的基礎(chǔ)代碼。當(dāng)視圖加載的時(shí)候,我們會(huì)從 Nib
中加載 cell;我們定制 NibName
,然后我們使用一個(gè) ReuseIdentifier 來注冊(cè) Nib
。
let foodCellNib = UINib(NibName: String(FoodTableViewCell), bundle: nil)
tableView.registerNib(foodCellNib,
forCellReuseIdentifier: String(FoodTableViewCell))
不幸的是,因?yàn)?UIKit 創(chuàng)建方式的限制,我們不得不使用字符串。我喜歡為我的 cell 使用相同的 identifiers 來作為 cell 的名字。
我們立刻就能看到低效的地方。如果你以前使用的是 Objective-C,我常常使用 NSString
作為類。在 Swift 里面,你可以使用 String
(稍好一點(diǎn)),相較 Objective-C 而言,我們已經(jīng)足夠好了。我們常常就使用 String
,但是如果一個(gè)沒有做過 iOS 開發(fā)的實(shí)習(xí)生來到我們的項(xiàng)目,這個(gè)函數(shù)對(duì)他來說就是天書。你會(huì)隨機(jī)的字符串化一些名字,”為什么你這樣做呢?”。同時(shí),如果你不在 storyboard 里指定 identifier 的話,現(xiàn)在它會(huì) crashing 而且他們還不知道什么原因。我們?cè)撊绾胃倪M(jìn)呢?
protocol ReusableView: class {}
extension ReusableView where Self: UIView {
static var reuseIdentifier: String {
return String(self)
}
}
extension UITableViewCell: ReusableView { }
FoodTableViewCell.reuseIdentifier
// FoodTableViewCell
因?yàn)槲覀儾辉偈褂?Objective-C 了,我們可以為這些 cell 重用視圖協(xié)議。
let foodCellNib = UINib(NibName: "FoodTableViewCell",
bundle: nil)
tableView.registerNib(foodCellNib,
forCellReuseIdentifier: FoodTableViewCell.reuseIdentifier)
protocol NibLoadableView: class { }
extension NibLoadableView where Self: UIView {
static var NibName: String {
return String(self)
}
}
再說一次,表格視圖里面每一個(gè)單獨(dú)的復(fù)用 identifier 都會(huì)變成類的字符串版本。我們可以對(duì)每一個(gè)視圖使用協(xié)議擴(kuò)展。這對(duì)UICollectionView
UITableView
Cell
也適用。這是我們的可復(fù)用的 identifier。我們可以把這個(gè)不得不用的討厭邏輯封裝起來. 因?yàn)?UIKit
需要它。現(xiàn)在我們能擴(kuò)展每一個(gè)單獨(dú)的 UITableViewCell
了。
extension FoodTableViewCell: NibLoadableView { }
FoodTableViewCell.NibName
// "FoodTableViewCell"
我們可以對(duì) UICollectionViewCell
做同樣的事情來擴(kuò)展可復(fù)用的視圖協(xié)議。每一個(gè)單獨(dú)的 cell 都有一個(gè)默認(rèn)的 reuseIdentifier
,我們?cè)俨恍枰斎胍槐榛蛘邠?dān)心了。我們說, FoodTableViewCell
、 reuseIdentifier
,它將會(huì)通過字符串化類來幫助我們完成這件事情。
這依舊很長(zhǎng),但是更易讀:
let foodCellNib = UINib(NibName: FoodTableViewCell.NibName, bundle: nil)
tableView.registerNib(foodCellNib,
forCellReuseIdentifier: FoodTableViewCell.reuseIdentifier)
extension UITableView {
func register<T: UITableViewCell where T: ReusableView, T: NibLoadableView>(_: T.Type) {
let Nib = UINib(NibName: T.NibName, bundle: nil)
registerNib(Nib, forCellReuseIdentifier: T.reuseIdentifier)
}
}
我們也可以對(duì) NibName
做同樣的事情,因?yàn)槲覀儾幌胩幚碜址N覀兡軇?chuàng)建一個(gè) NibLoadableView
(任何能從 Nib 里加載的類)。我們會(huì)有一個(gè) NibName
,而且它會(huì)返回類名的字符串版本。
let foodCellNib = UINib(NibName: "FoodTableViewCell", bundle: nil)
tableView.registerNib(foodCellNib,
forCellReuseIdentifier: "FoodTableViewCell")
如何從 Nib
里面加載的視圖,比如我們的 TableViewCell
,將會(huì)遵循 Nib
可加載視圖協(xié)議。它會(huì)自動(dòng)地有一個(gè) NibName
的屬性,而且會(huì)字符串化類名。至少實(shí)習(xí)生能明白我們現(xiàn)在有了 cell 的 NibName
,它是這個(gè) cell 的 reuseIdentifier
,而且每一次我們注冊(cè)這個(gè)類的時(shí)候,每一個(gè) TableViewCell
都是這樣的。
我們現(xiàn)在能再進(jìn)一步,使用泛型來注冊(cè)我們的 cells,然后提取這兩行代碼。
tableView.register(FoodTableViewCell)
我們可以擴(kuò)展我們的 tableView
然后創(chuàng)建一個(gè)注冊(cè)類,這個(gè)類可以接收一個(gè)類型包含這兩種協(xié)議。它有一個(gè)可重用的標(biāo)識(shí)符和一個(gè)從那些協(xié)議里面獲取的 Nib
名字。現(xiàn)在我們可以完整地從遵循 NibLoadableView
要求的 Nib
名字的位置,抽取這兩行代碼的邏輯出來。我們知道它有一個(gè)叫做 NibName
的屬性,而且 cell 會(huì)遵循可重用的視圖協(xié)議 (它們會(huì)有可重用的標(biāo)識(shí)符屬性)。這兩行代碼,本來我們需要在每一個(gè)單獨(dú)的表格視圖里面都輸入一遍,現(xiàn)在被抽取出來了。只需要一行代碼,我們就完成 cell 的注冊(cè),這看起來會(huì)干凈許多。你不需要再處理字符串了。
我們可以更進(jìn)一步。我們不得不注冊(cè) cells,我們也不得不清理 cells。我們可以用泛型和協(xié)議來代替這些本來很丑的代碼:當(dāng)你需要清理的時(shí)候,你需要指明 reuseIdentifier
。在 Swift 里面,這只需要三行代碼,因?yàn)槲覀冇?optionals。
extension UITableView {
func dequeueReusableCell<T: UITableViewCell where T: ReusableView>(forIndexPath indexPath: NSIndexPath) -> T {
guard let cell = dequeueReusableCellWithIdentifier(T.reuseIdentifier, forIndexPath: indexPath) as? T else {
fatalError("Could not dequeue cell with identifier: \(T.reuseIdentifier)")
}
return cell
}
}
當(dāng)你清理一個(gè) cell 的時(shí)候,你需要這個(gè)保證聲明,這是一個(gè) cell 的清理;如果沒有這個(gè)聲明,你或者有一個(gè)嚴(yán)重的錯(cuò)誤,或者一個(gè) explicit unwrapping。
guard let cell = tableView.dequeueReusableCellWithIdentifier(“FoodTableViewCell", forIndexPath: indexPath)
as? FoodTableViewCell
else {
fatalError("Could not dequeue cell with identifier: FoodTableViewCell")
}
當(dāng)你輸入這行代碼的時(shí)候,你都會(huì)覺得丑陋。它源自 Objective-C,我們從 UIKit 里面開始有它,我們對(duì)它沒有太多的辦法。但是使用協(xié)議,我們可以抽取這些丑陋的地方,因?yàn)槲覀儗?duì)每一個(gè)單獨(dú)的表格視圖 cell 都有 reuseIdentifier
。
let cell = tableView.dequeueReusableCell(forIndexPath: indexPath) as FoodTableViewCell
我們能如下實(shí)現(xiàn)上面這段代碼的功能:
if indexPath.row == 0 {
return tableView.dequeueReusableCell(forIndexPath: indexPath) as DesertTableViewCell
}
return tableView.dequeueReusableCell(forIndexPath: indexPath) as FoodTableViewCell
每一次我們清理一個(gè) cell,我們都能這樣做。我們?cè)?forIndexPath
里清理 cell,而且我們說明 cell 是哪個(gè)。如果你有多個(gè) cells,你可以把它轉(zhuǎn)換成你注冊(cè)的那個(gè) cell,它馬上就會(huì)知道它的類型是什么了。
這太神奇了! 這是個(gè)替代原來我們?cè)?Objective-C 里方式的好方法,這個(gè)方法混合了 Swift 和 optionals,并采用了協(xié)議和泛型,給我們的項(xiàng)目帶來更好看的代碼。
iOS Cell 注冊(cè) & 用 Swift 協(xié)議擴(kuò)展和泛型來實(shí)現(xiàn)復(fù)用
這部分源自 Guille Gonzalez,他把這個(gè)原則???用到 collection view 上,你也可以把這個(gè)方法運(yùn)用到其他你有問題的 UIKit 的組件上,例如 Swift 中面向協(xié)議的 Segue 標(biāo)識(shí)符。你可以在每天的編程中都像那樣使用協(xié)議,這樣也會(huì)安全些。它也是源自 Apple 去年 WWDC 上的例子。面向協(xié)議編程真的很棒。
網(wǎng)絡(luò)
使用網(wǎng)絡(luò)的時(shí)候,你一般要調(diào)用 API。 我常常這樣做:我有一些服務(wù) (比如 我從服務(wù)器那獲取食物),我有一個(gè) get
函數(shù),它會(huì)調(diào)用 API 然后得到結(jié)果。我想使用 Swift 的錯(cuò)誤處理,但是它是異步的,我不能拋出錯(cuò)誤。
struct FoodService {
func get(completionHandler: Result<[Food]> -> Void) {
// make asynchronous API call
// and return appropriate result
}
}
我將使用結(jié)果枚舉,這是在Swift里面源自 Haskel 的常見模式。
結(jié)果枚舉很簡(jiǎn)單。當(dāng)服務(wù)器返回結(jié)果的時(shí)候,我們能把它解析為成功然后返回一個(gè)食物條目的數(shù)組。如果失敗了,我們能返回一個(gè)錯(cuò)誤碼,然后完成句柄中的 view controller 會(huì)知道如何處理這些情況。
enum Result<T> {
case Success(T)
case Failure(ErrorType)
}
當(dāng)服務(wù)器異步返回結(jié)果的時(shí)候,這使用了我們的完成句柄,我們將傳入食物條目的結(jié)果。
struct FoodService {
func get(completionHandler: Result<[Food]> -> Void) {
// make asynchronous API call
// and return appropriate result
}
}
現(xiàn)在 view controller 將會(huì)解析它們。我們?cè)?view controller 里有一個(gè) dataSource
。當(dāng)視圖加載的時(shí)候,我們將調(diào)用異步 API,然后再完成句柄中得到結(jié)果。如果結(jié)果是一組食物,太棒了:我們重置數(shù)據(jù),重新加載表格視圖。如果結(jié)果是個(gè)錯(cuò)誤,我們會(huì)給用戶一個(gè)錯(cuò)誤提示,然后處理它。
// FoodLaLaViewController
var dataSource = [Food]() {
didSet {
tableView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
getFood()
}
private func getFood() {
FoodService().getFood() { [weak self] result in
switch result {
case .Success(let food):
self?.dataSource = food
case .Failure(let error):
self?.showError(error)
}
}
}
這是一個(gè)典型的調(diào)用 API 的模式。但是整個(gè) view controller 依賴上食物數(shù)組的加載了:如果沒有數(shù)據(jù)或者數(shù)據(jù)錯(cuò)誤,它會(huì)失敗。確認(rèn) view controller 是按預(yù)期正確處理了數(shù)據(jù)的最好方式是……測(cè)試。
View Controller 測(cè)試?!!!
View Controller 測(cè)試很痛苦。在這個(gè)例子中,因?yàn)槲覀冇辛朔?wù),異步 API 調(diào)用,一個(gè)完成代碼塊,和一些結(jié)果枚舉,測(cè)試就會(huì)更加痛苦。這些都使得測(cè)試 view controller 是否按預(yù)期工作變得更加困難。
// FoodLaLaViewController
var dataSource = [Food]() {
didSet {
tableView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
getFood()
}
private func getFood() {
FoodService().getFood() { [weak self] result in
switch result {
case .Success(let food):
self?.dataSource = food
case .Failure(let error):
self?.showError(error)
}
}
}
首先,我們需要對(duì) food 服務(wù)有更多的控制;我們需要能夠給它注入一個(gè)食物的數(shù)組或者一個(gè)錯(cuò)誤。我們能看到問題了:當(dāng) getFood()
實(shí)例化一個(gè) food 服務(wù)的時(shí)候,我們的測(cè)試沒有機(jī)會(huì)能注入。第一個(gè)測(cè)試是增加依賴注入。
// FoodLaLaViewController
func getFood(fromService service: FoodService) {
service.getFood() { [weak self] result in
// handle result
}
}
// FoodLaLaViewControllerTests
func testFetchFood() {
viewController.getFood(fromService: FoodService())
// now what?
}
現(xiàn)在我們的 getFood()
函數(shù)接收 FoodService
參數(shù),這樣我們就有了更多的控制權(quán)了,之后我們才能做更多的測(cè)試。我們有 controller
,叫做 getFood
函數(shù),然后我們給它傳入 FoodService
。當(dāng)然,我們想要對(duì)于 FoodService
完整的控制。 我們?nèi)绾螌?shí)現(xiàn)呢?
struct FoodService {
func get(completionHandler: Result<[Food]> -> Void) {
// make asynchronous API call
// and return appropriate result
}
}
這是一個(gè)值類型:你不能有子類。相反,你需要用協(xié)議。 FoodService
有一個(gè) get 函數(shù),completionHandler
會(huì)給出結(jié)果。你可以想象你應(yīng)用里面的每一個(gè)服務(wù),每一個(gè) API 調(diào)用都需要一個(gè) get 函數(shù) (比如 dessert),也會(huì)有類似的東西。它有一個(gè)完成句柄能夠接收結(jié)果,然后解析它。
我們馬上能讓它變得更通用:
protocol Gettable {
associatedtype T
func get(completionHandler: Result<T> -> Void)
}
我們能使用協(xié)議和相關(guān)的類型 (Swift 里面使用泛型的方式)。我們說每一個(gè)遵循 Gettable
協(xié)議的地方都有 get 函數(shù),而且它接收一個(gè)完成句柄和這個(gè)類型的結(jié)果。在我們的例子中,這會(huì)是 food (但是在 dessert 服務(wù)中,它會(huì)是 dessert;這是能互相交換的)。
struct FoodService: Gettable {
func get(completionHandler: Result<[Food]> -> Void) {
// make asynchronous API call
// and return appropriate result
}
}
回到 food 服務(wù),唯一的改變就是它需要遵循 Gettable
協(xié)議。 get 函數(shù)已經(jīng)實(shí)現(xiàn)了。它只需要接收一個(gè) completionHandler
,這個(gè)句柄接收結(jié)果的條目……因?yàn)橄嚓P(guān)類型的協(xié)議是智能的 (結(jié)果是 food 數(shù)組,相關(guān)類型就是 food 數(shù)組)。你不需要描述它。
回到 view controller,這基本上就是一樣的了。
// FoodLaLaViewController
override func viewDidLoad() {
super.viewDidLoad()
getFood(fromService: FoodService())
}
func getFood<S: Gettable where S.T == [Food]>(fromService service: S) {
service.get() { [weak self] result in
switch result {
case .Success(let food):
self?.dataSource = food
case .Failure(let error):
self?.showError(error)
}
}
}
唯一的區(qū)別就是你需要的相關(guān)類型只能是 food 數(shù)組 (你可不希望我們的 food view controller 調(diào)用 dessert 服務(wù))。你想限制它,然后說明這個(gè) getFood()
函數(shù)只能獲得 food 條目的結(jié)果。否則,它就是其他遵循 Gettable
協(xié)議的東西。這使得我們能對(duì)傳入的,諸如 FoodService
的參數(shù)有更強(qiáng)的控制 – 因?yàn)樗恍枰欢ㄊ?FoodService
,我們能注入其他的東西。
在我的測(cè)試中,我們創(chuàng)建了一個(gè) Fake_FoodService
。它有兩個(gè)事情: 1) 遵循 Gettable
協(xié)議,2) 相關(guān)類型需要是 food 數(shù)組。
// FoodLaLaViewControllerTests
class Fake_FoodService: Gettable {
var getWasCalled = false
func get(completionHandler: Result<[Food]> -> Void) {
getWasCalled = true
completionHandler(Result.Success(food))
}
}
它遵循 Gettable
,它接收 food 的結(jié)果,然后返回一個(gè) food 數(shù)組。因?yàn)檫@是測(cè)試,我們想確定 Gettable
的 get 函數(shù)被調(diào)用了,因?yàn)樗芊祷兀液瘮?shù)理論上可以分配一個(gè)從任意地方獲取的 food 條目的數(shù)組。我們需要保證它被調(diào)用到;在這個(gè)例子里面是成功的例子,但是你能通過注入失敗來完成同樣的對(duì) view controller 的測(cè)試,來保證你的 view controller 的行為在你的輸入結(jié)果的條件下是正常的。測(cè)試如下:
// FoodLaLaViewControllerTests
func testFetchFood() {
let fakeFoodService = Fake_FoodService()
viewController.getFood(fromService: fakeFoodService)
XCTAssertTrue(fakeFoodService.getWasCalled)
XCTAssertEqual(viewController.dataSource.count, food.count)
XCTAssertEqual(viewController.dataSource, food)
}
我們有 fakeFoodService
:我們能注入我們的 fakeFoodService
(這個(gè)我們有更強(qiáng)的控制力),而且我們能測(cè)試 get 函數(shù)能被調(diào)用到,而且通過我們的 FoodService
注入的數(shù)據(jù)源和我們賦給 view controller 的數(shù)據(jù)源是同組數(shù)據(jù)。通過增加 Gettable
協(xié)議,我們有了一個(gè)對(duì) view controller 的強(qiáng)大測(cè)試,我們有了對(duì)所有服務(wù)的測(cè)試框架。我們能實(shí)現(xiàn)一個(gè)可刪除的,可更新的,可創(chuàng)建的協(xié)議;關(guān)于服務(wù),你能馬上看出哪個(gè)函數(shù)需要被實(shí)現(xiàn)而且容易注入,然后測(cè)試它們。
我用協(xié)議寫了一個(gè)注入 storyboards 的例子,而且我強(qiáng)烈推薦 Alexis Gallagher 的這篇演講 相關(guān)類型的協(xié)議. 我簡(jiǎn)化了它,但是相關(guān)類型的協(xié)議也會(huì)常常出乎意料。使用它時(shí),你可能會(huì)感到沮喪,這篇文章會(huì)使你平靜些,因?yàn)樗忉屃怂南拗啤?/p>
然后,你可以回來,享受爆米花了。
POP 實(shí)踐!結(jié)論
我們討論了協(xié)議是如何在實(shí)際工作中運(yùn)用的,特別是在每天編碼過程中是如何把它使用到視圖控制器,視圖和網(wǎng)絡(luò)中去的。本篇演講幫助你編寫出安全的,可維護(hù)的,可重用的,更統(tǒng)一的,模塊化代碼。更加易于測(cè)試的代碼。相比于子類而言,協(xié)議更棒。
然而,協(xié)議也會(huì)被濫用。我可能使用了過多的協(xié)議了:我學(xué)習(xí)它,它是個(gè)全新的東西而且吸引眼球,我希望無時(shí)不刻都使用它……但是這是不必要的。在我的第一個(gè)例子里面,當(dāng)我有一個(gè)抖動(dòng)的視圖和抖動(dòng)函數(shù)的時(shí)候,這就很棒。只有在兩個(gè)視圖都需要抽象的時(shí)候,我需要重構(gòu)它們的時(shí)候,放到協(xié)議里才是合理的。不要瘋狂地使用協(xié)議。
作為結(jié)束,兩個(gè)非常有趣的演講:
Beyond Crusty: 真實(shí)世界的協(xié)議 作者:Rob Napier,基于真實(shí)世界的協(xié)議。他一開始介紹了些壞的代碼,用協(xié)議重構(gòu)了它們……使用了 10 個(gè)協(xié)議和 20 行代碼。他通過結(jié)構(gòu)解決了問題,僅僅用了四行代碼。在 Swift 里,我們有不同的新的東西,包括強(qiáng)大的枚舉,結(jié)構(gòu)和協(xié)議。基于具體問題,可能需要不同的解決方案。我推薦用協(xié)議實(shí)驗(yàn),但是還是要想想,”這個(gè)問題結(jié)構(gòu)能行嗎,或者組合就足夠了?”。
Blending Cultures: 函數(shù)式,面向協(xié)議和面向?qū)ο缶幊痰淖罴褜?shí)踐 作者: Daniel Steinberg。他從你如何使用每個(gè)方法開始,因?yàn)槲覀儾皇芟抻趨f(xié)議,我們?nèi)匀挥忻嫦驅(qū)ο蟮乃枷牒秃瘮?shù)式思想。這是一個(gè)很棒的演講,它展示了如何使用一切手段來完成你的代碼中不變部分和變化部分的抽取。
在你每天編程的時(shí)候,希望你能考慮使用協(xié)議!