Swift面向協(xié)議編程

對(duì)面向協(xié)議不熟悉的swift開發(fā)者,個(gè)人感覺這篇文章寫得很好,適合面向協(xié)議編程的初學(xué)者。
原文作者:http://www.tuicool.com/articles/AzAZvqQ

簡單的任務(wù)

假設(shè)你要寫一個(gè)由一張圖片和一個(gè)按鈕構(gòu)成的簡單應(yīng)用,產(chǎn)品經(jīng)理希望按鈕被點(diǎn)擊的時(shí)候圖片會(huì)抖動(dòng),就像這樣:


aAjQ7rQ.gif

由于這個(gè)動(dòng)畫常常在用戶名或者密碼輸入錯(cuò)誤時(shí)被用到,所以我們很容易就能 在 StackOverflow 上找到代碼 (就像每個(gè)好的開發(fā)者都會(huì)做的一樣:grin:)
這個(gè)需求最難的地方就是決定實(shí)現(xiàn)抖動(dòng)的代碼應(yīng)該寫在哪兒,但這其實(shí)也沒多難。我寫了個(gè) UIImageView 的子類,再給它加上一個(gè) shake() 方法就搞定了。

//  FoodImageView.swift
 
import UIKit
 
class FoodImageView: UIImageView {
    
    // shake() 方法寫在這兒
    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")
    }
}

現(xiàn)在,當(dāng)用戶點(diǎn)擊按鈕的時(shí)候,我只要調(diào)用 ImageView 的 shake 方法就行了:

//  ViewController.swift
 
import UIKit
 
class ViewController: UIViewController {
 
    @IBOutlet weak var foodImageView: FoodImageView!
    
    @IBAction func onShakeButtonTap(sender: AnyObject) {
        // 在這里調(diào)用 shake 方法
        foodImageView.shake()
    }
}

這并沒什么令人激動(dòng)的。任務(wù)完成,現(xiàn)在我可以繼續(xù)處理別的任務(wù)了……感謝 StackOverflow!

功能拓展

然而,就像實(shí)際開發(fā)中會(huì)發(fā)生的那樣,當(dāng)你認(rèn)為你搞定了任務(wù),可以繼續(xù)下一項(xiàng)的時(shí)候,設(shè)計(jì)師跳了出來告訴你他們希望按鈕能夠和 ImageView 一起抖動(dòng)……

bU73amV.gif

當(dāng)然,你可以重復(fù)上面的做法–寫個(gè) UIButton 的子類,再加個(gè) shake 方法:

//  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")
    }
 
}

現(xiàn)在,當(dāng)用戶點(diǎn)擊按鈕的時(shí)候,你就可以讓 ImageView 和按鈕一起抖動(dòng)了:

//  ViewController.swift
 
class ViewController: UIViewController {
 
    @IBOutlet weak var foodImageView: FoodImageView!
    @IBOutlet weak var actionButton: ActionButton!
    
    @IBAction func onShakeButtonTap(sender: AnyObject) {
        foodImageView.shake()
        actionButton.shake()
    }
}

但愿你沒這么做……在兩個(gè)地方重復(fù)編寫 shake() 方法違背了 DRY(don’t repeat yourself)原則。如果之后一個(gè)設(shè)計(jì)師又過來表示需要更多或者更少的視圖進(jìn)行抖動(dòng),你就不得不在多處修改邏輯,這樣當(dāng)然并不理想。
所以該如何重構(gòu)呢?

通常的處理方式

如果你寫過 Objective-C, 你很可能會(huì)把 shake() 寫到一個(gè) UIView 的分類(Category) 中(也就是 Swift 中的拓展 (extension)):

//  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")
    }
}

現(xiàn)在,UIImageView 和 UIButton(以及其他所有視圖)都有了可用的 shake() 方法:

class FoodImageView: UIImageView {
    // 其他自定義寫在這兒
}
 
class ActionButton: UIButton {
    // 其他自定義寫在這兒
}
 
class ViewController: UIViewController {
 
    @IBOutlet weak var foodImageView: FoodImageView!
    @IBOutlet weak var actionButton: ActionButton!
    
    @IBAction func onShakeButtonTap(sender: AnyObject) {
        foodImageView.shake()
        actionButton.shake()
    }
}

然而,你立刻就會(huì)發(fā)現(xiàn),在 FoodImageView 或者 ActionButton 的代碼中并沒有什么特別的東西表示它們能夠抖動(dòng)。只是因?yàn)槟銓懥四莻€(gè)拓展(或分類),你知道有那么一個(gè)能實(shí)現(xiàn)抖動(dòng)的方法被放在其中某處。

再進(jìn)一步說,這種分類模式很容易就會(huì)失控。分類容易變成一個(gè)垃圾桶,以存放那些你不知道該放到哪里的代碼。很快,分類里的東西就太多了,你甚至都不知道一些代碼為什么在那兒,又該用在哪兒?
所以,該怎么做呢……:thought_balloon:

用協(xié)議(Protocol)來搞定!

你猜對(duì)了!Swifty 的解決方案就是用協(xié)議!我們能夠利用協(xié)議拓展的力量來創(chuàng)建一個(gè)帶有默認(rèn) shake() 方法實(shí)現(xiàn)的 Shakeable 協(xié)議:

//  Shakeable.swift
 
import UIKit
 
protocol Shakeable { }

// 你可以只為 UIView 添加 shake 方法!
extension Shakeable where Self: UIView {
    
    // shake 方法的默認(rèn)實(shí)現(xiàn)
    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")
    }
}

現(xiàn)在,我們只需要讓任何確實(shí)需要抖動(dòng)的視圖遵從 Shakeable 協(xié)議就好了:

class FoodImageView: UIImageView, Shakeable {
    // 其他自定義寫在這兒
}
 
class ActionButton: UIButton, Shakeable {
    // 其他自定義寫在這兒
}
 
class ViewController: UIViewController {
 
    @IBOutlet weak var foodImageView: FoodImageView!
    @IBOutlet weak var actionButton: ActionButton!
    
    @IBAction func onShakeButtonTap(sender: AnyObject) {
        foodImageView.shake()
        actionButton.shake()
    }
}

這里需要注意的第一點(diǎn)是可讀性!僅僅通過 FoodImageView 和 ActionButton 的類聲明,你就能立刻知道它能抖動(dòng)。

如果設(shè)計(jì)師跑過來表示希望在抖動(dòng)的同時(shí) ImageView 能暗淡一點(diǎn)兒,我們也能夠利用相同的協(xié)議拓展模式添加新的功能,進(jìn)行超級(jí)贊的功能組合。

// 添加暗淡功能
class FoodImageView: UIImageView, Shakeable, Dimmable {
    // 其他實(shí)現(xiàn)寫在這兒
}

而且,當(dāng)產(chǎn)品經(jīng)理不再想讓 ImageView 抖動(dòng)的時(shí)候,重構(gòu)起來也超級(jí)簡單。只要移除對(duì) Shakeable 協(xié)議的遵從就好了!

class FoodImageView: UIImageView, Dimmable {
    // 其他實(shí)現(xiàn)寫在這兒
}

結(jié)論

使用協(xié)議拓展來構(gòu)造視圖, 你就為你的代碼庫增加了超級(jí)棒的 可讀性 , 復(fù)用性 和 可維護(hù)性

譯者注,原文評(píng)論中有人認(rèn)為 “面向協(xié)議的視圖” 并沒必要,增加了過多的代碼(每個(gè)功能都要寫個(gè)協(xié)議)及不必要的代碼層次(分類/拓展的話是 類 -> 方法,而協(xié)議是 類 -> 協(xié)議 -> 方法),一般的需求沒必要這樣,并提供了一個(gè)演講供參考,演講大意是避免不必要的層層封裝,保持簡單實(shí)現(xiàn),代碼的未來的拓展什么的自然有維護(hù)團(tuán)隊(duì)(=,=?)做等等。另外也有其他讀者對(duì)之進(jìn)行了反駁,感興趣可以看看。個(gè)人還是支持作者的觀點(diǎn)。

本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán),最新文章請(qǐng)?jiān)L問http://swift.gg。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容