Swift 面向Protocol編程淺析:Inheritence-Composition-Protocol

記得從大學學編程開始,對于軟件編程聽的最多的就是面向?qū)ο缶幊?Object Oriented Programming匕得,OOP)了,它的三大特征:封裝,繼承,多態(tài).而Swift倡導的面向協(xié)議編程(Protocol-oriented programming,POP)是OOP的一個范例,我理解為"封裝+協(xié)議*結(jié)構(gòu)體+擴展"(Swift2.0開始,你可以擴展一個protocol)

WWDC:Protocol-Oriented Programming in Swift開頭Classess Are Awesome,指出OOP中的Classes提供了:數(shù)據(jù)的封裝、訪問控制、抽象化运翼、命名空間等,但這些都是Classes才特有的屬性嗎?事實上,這些都是類型(Type)的所有屬性,Classes只是Type的一種實現(xiàn)方法,在Swift中I can do all that with structs and enums(Swift的標準庫組成:55個Protocols和102個Structs),這一點可以理解為封裝性.而繼承和多態(tài),是structenum不具備的,它則是通過遵守protocol來實現(xiàn).

但,這些是為了說明讓我們放棄OOP嗎?這是不可能的....想想UIKit....記得剛開始用Swift寫項目時,總是告誡自己,不能只是機械的把objc 翻譯成 swift.<font color="IndianRed">實際開發(fā)項目中,ViewControlView基本都是使用系統(tǒng)的框架,通過繼承來實現(xiàn),無論如何的自定義,都是要圍繞蘋果的那一套來,OC與swift在這一塊保持一致;但在modelhandle/viewModel/manager這一塊,更多的通過POP實現(xiàn),后面會通過一個例子來說明.</font>(PS:在oc中,我們體會的是OOP/FRP(參考一下RAC),那Swift就是OOP/FRP(參考一下RxSwift)/POP;在oc中對于Protocols的理解更多的是UIAppplicationDelegate,UITableViewDelegete,NSCopying,UITextFieldDelegate....,而Swift中Protocols則被賦予了更多的功能和意義:"可定義屬性,可組合,可繼承,可擴展,支持泛型,支持類/結(jié)構(gòu)體/枚舉").在Swift面向協(xié)議編程初探中,bz總結(jié)的一句話,非常nice:
面向?qū)ο缶幊毯兔嫦騾f(xié)議編程最明顯的區(qū)別在于程序設(shè)計過程中對數(shù)據(jù)類型的抽取(抽象)上,面向?qū)ο缶幊淌褂妙惡屠^承的手段灶泵,數(shù)據(jù)類型是引用類型
面向協(xié)議編程使用的是遵守協(xié)議的手段货矮,數(shù)據(jù)類型是值類型(Swift中的結(jié)構(gòu)體或枚舉)
PS:值類型引用類型的區(qū)別這里不作詳敘,可參考Swift:什么時候使用結(jié)構(gòu)體和類

討厭的"上帝"(Inheritence)

繼承帶給我們最大的問題,可能就是常常會構(gòu)造出所謂的God類/super類,帶來的壞處也隨之可見:

  • 一層一層一層的傳遞下去,它的任何行為都會影響它的所有小弟;
  • 有的小弟繼承了無用的屬性和方法;
  • 不方便擴展,差別不大的同類上帝,直接拷貝一遍代碼?
    特別喜歡田偉宇博客:跳出面向?qū)ο笏枷?一) 繼承中提到關(guān)于繼承的要點之一:父類的所有變化炫彩,都需要在子類中體現(xiàn)匾七,也就是說此時耦合已經(jīng)成為需求.(他的文章非常nice,在架構(gòu)這一塊的寫的系列文章值得深讀)so,LZ的觀點也是萬不得已不要用繼承,優(yōu)先考慮組合!
    注:在objc中,更多的是用組合(Composition),在Swift中則是協(xié)議>組合>繼承.后面會舉例說明.
    再注:全文的Demo在這里
    我們通過兩張圖對比一下:引用自程序員聊人生

// 父類
class Animal {
    var name: String = ""
    var type: String = ""
    func eat(){} 
//    func fly(){}
}

class Bird: Animal {
    func fly(){
        print("Bird can fly")
    }
}

class Preson: Animal {
    func speak(){
        print("person can speak")
    }
}

class Fish: Animal {
    func swimming() {
        print("fish can swimming")
    }
}

// 假設(shè)超人會飛不會游泳,復制飛的方法
class SuperMan: Preson {
    override func speak() {
        print("superman also speak")
    }
    func fly(){
        print("superman also fly")
    }
}

class SuperFishMan: SuperMan {
    func swimming() {
        print("superfishman can swimming")
    }
}
  • objc/Swift都不存在多繼承,會游泳的超人,這時要復制游的方法,到這里已經(jīng)是第四層了...高耦合
  • 也不好直接把fly()定義到父類Animal中,等于強加限制.因為通過繼承,抽象出共同的性質(zhì),Bird/Preson/Fish都是動物(人是高級動物),它們都有屬性name和type,都具有eat()的行為,但fly()不是所有動物共有的
  • 這時來了一個外星生物,它不屬于Animal,但是擁有Animal及其子類所有的屬性和行為(方法),怎么辦?上帝類都幫不了你了,又走上了重復復制之路!

有句話是咋說的:我們區(qū)分鳥和魚,不是因為它們的名字是鳥/魚,而是通過它們表現(xiàn)的行為,有點亂,_.把所有的行為拆分出來,通過搭積木的形式組合出來,你具備什么就拿什么,那么你的身份也就隨之浮現(xiàn)了.

protocol Property {
    var name: String {get}
    var type: String {get}
}

extension Property {
    var name: String {
        return "超人"
    }
    var type: String {
        return "外星類"
    }
}

protocol Speaker {
    func speak()
}

protocol Flyer {
    func fly()
}

protocol Swimer {
    func swimming()
}

struct SuperMan {
}

extension SuperMan: Property,Flyer,Speaker {
    func fly() {
        print("superman also fly")
    }
    func speak() {
        print("superman also speak")
    }
}

struct SuperFishMan {
}

extension SuperFishMan: Property,Flyer,Speaker,Swimer {
    var name: String {
        return "超水人"http:// 好蠢的名字...
    }
    func fly() {
        print("...")
    }
    func speak() {
        print("...")
        
    }
    func swimming() {
        print("....")
    }
}
  • objc中,還是可以定義一個父類Animal的,LZ現(xiàn)在基本都是寫Swift了,就直接定義了一個protocol:Property,在擴展中寫好默認實現(xiàn)
  • 消滅了上帝類,全部都定義為protocol,用到什么就拼接什么,真的就夠搭積木一樣便捷...

組合(Composition),哎喲不錯
第一個例子屬于對于model定義,接下來看一個view層所表現(xiàn)出的問題.
手機QQ底部tabbar的三個標簽首頁都帶有一個頭像控件,最開始我們采取繼承的形式來實現(xiàn)一個baseVC

class KQUserAvatarView: UIView {
}

class KQBaseViewController: UIViewController {
    var userAvatarView: KQUserAvatarView!

    func setupUserAvatarView() {
    }
    
    func clickOnAvatarView() {
    }
}
  • 新需求,希望第一、第二個標簽頁的頭像加上大V的標志,第三頁保持不變,此刻高耦合,父類改動牽動三個子類/甚至更多子類的變化.或許你直接在父類中添加改變樣式的方法,那么那些不需要改變的子類也就直接繼承了無用的方法...
  • 又來個需求,我需要一個父類是UITableViewController的新KQUserAvatarView,瞬間傻眼...只能復制代碼再創(chuàng)造一個上帝了
  • 這種情況還只是一個view的創(chuàng)建,如果是好幾個組合view的組成,那么VC中的代碼簡直就是災(zāi)難...

在objc/Swift1.2之前的,我們用組合來代替繼承,這是非常常見的一種做法.借助中間件,解耦+轉(zhuǎn)移邏輯代碼,減輕VC的負擔.

class KQUserAvatarView: UIView {
    var btn: UIButton!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

typealias ClickButtonAction = () -> ()
class KQUserAvatarViewManager {
    var userAvatarView: KQUserAvatarView!
    var tapHandle: ClickButtonAction?
    
    func setupUserAvatarViewAtContainView(view: UIView,tapHandle: ClickButtonAction?) {
        userAvatarView = KQUserAvatarView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
        userAvatarView.backgroundColor = UIColor.orangeColor()
        view.addSubview(userAvatarView)
        self.tapHandle = tapHandle
        userAvatarView.btn.addTarget(self, action: "clickOnAvatarView", forControlEvents: .TouchUpInside)
    }
    
    func clickOnAvatarView() {
        if let block = self.tapHandle {
            block()
        }
    }
}

class ViewController: UIViewController {
    var manager: KQUserAvatarViewManager!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        manager.setupUserAvatarViewAtContainView(self.view) {
            print("點擊了按鈕")
        }
        
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}
  • 哪個頁面需要頭像控件,直接創(chuàng)建一個KQUserAvatarViewManager對象進行引用就行,實現(xiàn)了解耦
  • 多個view對應(yīng)對個manager,很好的給VC進行了瘦身
  • 題外話:前端時間看了陽神的iOS 開發(fā)中的 Self-Manager 模式,評論也看了...對于文章所訴的觀點,LZ我也是贊同評論中提議的創(chuàng)建一個ViewManager,在它里面處理點擊事件或者delegate/block回調(diào)給VC來處理...至于這個 Avatar View 在 App 的各個地方都可能粗線江兢,而且行為一致昨忆,那就意味著事件處理的 block,要散落在各個頁面中杉允,同時也帶來了很多“只是為向上一層級轉(zhuǎn)發(fā)事件”的 “Middle Man”這句話,我認為,除非block中的處理事件完全一致(都是加載同一個model,都是push/modal推出視圖),否則做不到邏輯代碼只有一份的情況,它還是得分散在各個VC中做對應(yīng)的跳轉(zhuǎn)...(個人觀點,不喜勿噴)

POP的實現(xiàn)(Protocol)

如果說組合的缺點,調(diào)用時必須通過中間變量,管理它的創(chuàng)建和釋放,多了一層構(gòu)造(缺點是相對的,在POP之前都這樣用..優(yōu)點都是對比出來的)

typealias ClickButtonAction = () -> ()
class KQUserAvatarView: UIView {
    var btn: UIButton!
    var tapBlock: ClickButtonAction?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        btn = UIButton(type: .ContactAdd)
        btn.frame = CGRect(x: 0, y: 0, width: 40, height: 40)
        self.addSubview(btn)
        
        btn.addTarget(self, action: "clickOnAvatarView", forControlEvents: .TouchUpInside)
    }
    
    func clickOnAvatarView() {
        if let blcok = tapBlock {
            blcok()
        }
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

protocol UserAvatarViewAble: class {
    var userAvatarView: KQUserAvatarView! {get set}
    func setupUserAvatarView(tapHandle: ClickButtonAction?)
}

extension UserAvatarViewAble where Self: UIViewController {
    //  擴展不能實現(xiàn)儲存屬性
    func setupUserAvatarView(tapHandle: ClickButtonAction?) {
        userAvatarView = KQUserAvatarView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
        userAvatarView.backgroundColor = UIColor.orangeColor()
        self.view.addSubview(userAvatarView)
        userAvatarView.tapBlock = tapHandle
    }
}

class ViewController: UIViewController, UserAvatarViewAble {
    var userAvatarView: KQUserAvatarView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUserAvatarView {
            print("點擊了按鈕")
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}
  • 定義protocol,通過extension來實現(xiàn)協(xié)議,消除了中間變量
  • where Self: UIViewController用來規(guī)定只有采納協(xié)議的類型滿足這些限制條件時邑贴,才能獲得協(xié)議擴展提供的默認實現(xiàn).這個協(xié)議設(shè)計的就是作為VC的子視圖控件,因此可以用UIViewController來直接限定,soself.view.addSubview(xxx)可以直接在協(xié)議的擴展中完成.
  • UIViewController屬于類類型,因此協(xié)議UserAvatarViewAble必須用class關(guān)鍵字來修飾只能被類類型使用.還有就是在setupUserAvatarView方法中對屬性變量進行了修改,如果是結(jié)構(gòu)體/枚舉采用了協(xié)議,必須用mutating關(guān)鍵字來修飾方法,否則就會報錯...可以看看這個錯誤Protocol Extension, Mutating Function

這樣看上去是不是很簡潔易用?在Swift中很多場景都能通過它來實現(xiàn).比如:檢查手機號碼,用戶名的正則表達式判斷..../顏色,圖片的轉(zhuǎn)換...等一系列的邏輯方法.
LZ之前objc項目中就存在各種:WCXxxUtil,WCXxxHandle,WCRegularUtil...
遷移到Swift中就類似Mixins and Traits in Swift 2.0寫到的:

protocol ValidatesUsername {
    func isUsernameValid(password: String) -> Bool
}

extension ValidatesUsername {
    func isUsernameValid(username: String) -> Bool {
        if /* username too short */ {
            return false
        } else if /* username has invalid characters */ {
            return false
        } else {
            return true
        }
    }
}

class LoginViewController: UIViewController, ValidatesUsername, ValidatesPassword {
    @IBAction func loginButtonPressed() {
        if isUsernameValid(usernameTextField.text!) &&
            isPasswordValid(passwordTextField.text!) {
                // proceed with login
        } else {
            // show alert
        }
    }
}

protocol拆分了各種工具,extension實現(xiàn)默認設(shè)定,拿來即用,方便無污染.

POP在ViewModel中的體現(xiàn)
實現(xiàn)這樣一個功能,寫一個通訊錄,要有頭像和姓名-電話號碼...
protocol層(不記得在哪里看到,對于協(xié)議的命令用形容詞,果然IT最難的是命名...)

protocol PersonPresentAble {
    var nameTelText: String {get}
}

// 可以通過擴展提供默認實現(xiàn)...可用可不用
extension PersonPresentAble {
    var nameTelText: String {
        return "hehe"
    }
}

typealias TapImageViewAction = () -> ()
protocol ImagePresentAble {
    var showImage: UIImage? {get}
    var tapHandle: TapImageViewAction? {get}
}

ViewModel層

struct PersonModel {
    var firstName: String
    var lastName: String
    var fullName: String {
        return lastName + firstName
    }
    var telPhone: String
    var avatarImageUrl: String?
}

typealias TelPersonViewModelAble = protocol<PersonPresentAble,ImagePresentAble>
struct TelPersonViewModel: TelPersonViewModelAble {
    var telPerson: PersonModel
    var nameTelText: String
    var showImage: UIImage?
    var tapHandle: TapImageViewAction?
    
    init(model:PersonModel,tapHandle: TapImageViewAction?) {
        self.telPerson = model
        self.nameTelText = model.fullName + "  " + model.telPhone
        self.showImage = UIImage(named: model.avatarImageUrl!) // 暫時這樣,按道理是加載url,否則沒必要寫到viewmodel中
        self.tapHandle = tapHandle
    }
}
  • fullName直接寫成計算屬性比較方便,當然你也可以在viewmodel中拼接
  • 保留一個model屬性telPerson,因為有些賦值你不需要進行加工處理,比如年齡/身高
  • 雖然在PersonPresentAblenameTelText是get只讀的,但是實現(xiàn)起來仍能可寫.參見If a protocol requires a property to be gettable and settable, that property requirement cannot be fulfilled by a constant stored property or a read-only computed property. If the protocol only requires a property to be gettable, the requirement can be satisfied by any kind of property, and it is valid for the property to be also settable if this is useful for your own code.

View層和ViewController層

class ContactTableViewCell: UITableViewCell {
    @IBOutlet weak var telTextLabel: UILabel!
    @IBOutlet weak var avatarImageView: UIImageView!
    
    var tapHandle: TapImageViewAction?

    override func awakeFromNib() {
        super.awakeFromNib()
        let tapGesture = UITapGestureRecognizer(target: self, action: "tapAction")
        avatarImageView.addGestureRecognizer(tapGesture)
    }
    
    func configureDataWithViewModel(viewModel: TelPersonViewModelAble) {
        telTextLabel.text = viewModel.nameTelText
        avatarImageView.image = viewModel.showImage
        tapHandle = viewModel.tapHandle
    }
    
    func tapAction() {
        if let block = tapHandle {
            block()
        }
    }
}

// VC
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("hehe", forIndexPath: indexPath) as! ContactTableViewCell
    let testModel = PersonModel(firstName: "明濤", lastName: "胡", telPhone: "15279107716", avatarImageUrl: "麒麟星.jpg")
    let testViewModel = TelPersonViewModel(model: testModel) {
        print("我點擊了頭像")
    }
    cell.configureDataWithViewModel(testViewModel)
    return cell
}
  • 通過合成協(xié)議typealias TelPersonViewModelAble = protocol<PersonPresentAble,ImagePresentAble>來定義viewmodel的類型,代碼復用性高
  • 在objc中,viewmodel的類型常常容易被定死,存在共同屬性的時候又走上了繼承的老路了...比如:

    這時只需要另定義個protocol,無須寫父類弄繼承,依舊那句話,讓寫功能跟搭積木一樣:
protocol CompanyPresentAble {
    var positionText: String {get}
}

typealias InvestPersonViewModelAble = protocol<PersonPresentAble,ImagePresentAble,CompanyPresentAble>
...剩下的,你懂怎么寫的^_^

參考資料:
Mixins 比繼承更好
Swift中的協(xié)議編程
Introducing Protocol-Oriented Programming in Swift 2
Updated: Protocol-Oriented MVVM in Swift 2.0
Mixins and Traits in Swift 2.0
iOS應(yīng)用架構(gòu)談 view層的組織和調(diào)用方案

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市叔磷,隨后出現(xiàn)的幾起案子拢驾,更是在濱河造成了極大的恐慌世澜,老刑警劉巖独旷,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異封恰,居然都是意外死亡诺舔,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門莉恼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拌喉,“玉大人,你說我怎么就攤上這事俐银∧虮常” “怎么了?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵捶惜,是天一觀的道長田藐。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么汽久? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任鹤竭,我火速辦了婚禮,結(jié)果婚禮上回窘,老公的妹妹穿的比我還像新娘诺擅。我一直安慰自己,他們只是感情好啡直,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布烁涌。 她就那樣靜靜地躺著,像睡著了一般酒觅。 火紅的嫁衣襯著肌膚如雪撮执。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天舷丹,我揣著相機與錄音抒钱,去河邊找鬼。 笑死颜凯,一個胖子當著我的面吹牛谋币,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播症概,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼蕾额,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了彼城?” 一聲冷哼從身側(cè)響起诅蝶,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎募壕,沒想到半個月后调炬,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡舱馅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年缰泡,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片习柠。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡匀谣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出资溃,到底是詐尸還是另有隱情武翎,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布溶锭,位于F島的核電站宝恶,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜垫毙,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一霹疫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧综芥,春花似錦丽蝎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至额各,卻和暖如春国觉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背虾啦。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工麻诀, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人傲醉。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓蝇闭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親硬毕。 傳聞我的和親對象是個殘疾皇子丁眼,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353

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