很棒的文章 swift 協(xié)議 售碳、繼承的分析.

Mixins 比繼承更好

作者:Olivier Halligon匈勋,原文鏈接胆萧,原文日期:2015-11-08
譯者:ray16897188庆揩;校對:Cee;定稿:千葉知風

譯者注:MixinTrait 是面向?qū)ο缶幊陶Z言中的術(shù)語跌穗,本文中作者并未明確指出兩者之間的區(qū)別订晌。這兩個單詞在本譯文中也不做翻譯。

從面向?qū)ο蟮木幊陶Z言的角度來說蚌吸,繼承(Inheritence)總被用來在多個類之間共享代碼锈拨。但這并不總是一個最佳的解決方案,而且它本身還有些問題羹唠。在今天寫的這篇文章中推励,我們會看到 Swift 中的協(xié)議擴展(Protocol Extensions),并將其以「Mixins」的形式去使用是怎樣解決這個問題的肉迫。

你可以從這里下載包含本篇文章所有代碼的 Swift Playground

繼承本身存在的問題

假設(shè)你有個 app稿黄,里面有很多包含相同行為的 UIViewController 類喊衫,例如它們都有漢堡菜單。你當然不想在 app 中的每一個 View Controller 里都反復實現(xiàn)這個漢堡菜單的邏輯(例如設(shè)置 leftBarButtonItem 按鈕杆怕,點擊這個按鈕時打開或者關(guān)閉這個菜單族购,等等)。

解決方案很簡單陵珍,你只需要創(chuàng)建一個負責實現(xiàn)所有特定行為寝杖、而且是 UIViewController 的子類 CommonViewController。然后讓你所有的 ViewController 都直接繼承 CommonViewController 而不是 UIViewController 就可以了互纯,沒錯吧瑟幕?通過使用這種方式,這些類都繼承了父類的方法,且具有了相同的行為只盹,你也不用每次重復實現(xiàn)這些東西了辣往。

class CommonViewController: UIViewController {
 func setupBurgerMenu() { … }
 func onBurgerMenuTapped() { … }
 var burgerMenuIsOpen: Bool {
 didSet { … }
 }
}

class MyViewController: CommonViewController {
 func viewDidLoad() {
 super.viewDidLoad()
 setupBurgerMenu()
 }
}

但在隨后的開發(fā)階段,你會意識到自己需要一個 UITableViewController 或者一個 UICollectionViewController……暈死殖卑,CommonViewController 不能用了站削,因為它是繼承自 UIViewController 而不是 UITableViewController

你會怎么做孵稽,是實現(xiàn)和 CommonViewController 一樣的事情卻繼承于 UITableViewControllerCommonTableViewController 嗎许起?這會產(chǎn)生很多重復的代碼,而且是個十分糟糕的設(shè)計哦菩鲜。

組合(Composition)是救命稻草

誠然园细,解決這個問題,有句具有代表性并且正確的話是這么說的:

多用組合睦袖,少用繼承珊肃。

這意味著我們不使用繼承的方式,而是讓我們的 UIViewController 包含一些提供相應行為的內(nèi)部類(Inner class)馅笙。

在這個例子中伦乔,我們可以假定 BurgerMenuManager 類能提供創(chuàng)建漢堡菜單圖標、以及與這些圖標交互邏輯的所有必要的方法董习。那些各式各樣的 UIViewController 就會有一個 BurgerMenuManager 類型的屬性烈和,可以用來與漢堡餐單做交互。

class BurgerMenuManager {
 func setupBurgerMenu() { … }
 func onBurgerMenuTapped() { burgerMenuIsOpen = !burgerMenuisOpen }
 func burgerMenuIsOpen: Bool { didSet { … } }
}
class MyViewController: UIViewController {
 var menuManager: BurgerMenuManager()
 func viewDidLoad() {
 super.viewDidLoad()
 menuManager.setupBurgerMenu()
 }
}
class MyOtherViewController: UITableViewController {
 var menuManager: BurgerMenuManager()
 func viewDidLoad() {
 super.viewDidLoad()
 menuManager.setupBurgerMenu()
 } 
}

然而你能看出來這種解決方案會變得很臃腫皿淋。每次你都得去明確引用那個中間對象 menuManager招刹。

多繼承(Multiple inheritance)

繼承的另一個問題就是很多面向?qū)ο蟮木幊陶Z言都不支持多繼承(這兒有個很好的解釋,是關(guān)于菱形缺陷(Diamond problem)的)窝趣。

這就意味著一個類不能繼承自多個父類疯暑。

假如說你要創(chuàng)建一些科幻小說中的人物的對象模型。顯然哑舒,你得展現(xiàn)出 DocEmmettBrown妇拯,DoctorWhoTimeLord洗鸵,IronMan 還有 Superman 的能力……這些角色的相互關(guān)系是什么越锈?有些能時間旅行,有些能空間穿越膘滨,還有些兩種能力都會甘凭;有些能飛,而有些不能飛火邓;有些是人類丹弱,而有些不是……

IronManSuperman 這個兩個類都能飛德撬,于是我們就會設(shè)想有個 Flyer 類能提供一個實現(xiàn) fly() 的方法。但是 IronManDocEmmettBrown 都是人類砰逻,我們還會設(shè)想要有個 Human 父類;而 SupermanTimeLord 又得是 Alien的子類泛鸟。哦蝠咆,等會兒…… 那 IronMan 得同時繼承 FlyerHuman 兩個類嗎?這在 Swift 中是不可能的實現(xiàn)的(在很多其他的面向?qū)ο蟮恼Z言中也不能這么實現(xiàn))北滥。

我們應該從所有父類中選擇出符合子類屬性最好的一個么刚操?但是假如我們讓 IronMan 繼承 Human,那么怎么去實現(xiàn) fly() 這個方法再芋?很顯然我們不能在 Human 這個類中實現(xiàn)菊霜,因為并不是每個人都會飛,但是 Superman 卻需要這個方法济赎,然而我們并不想重復寫兩次鉴逞。

所以,我們在這里會使用組合(Composition)方法司训,讓 var flyingEngine: Flyer 成為 Superman 類中的一個屬性构捡。

但是調(diào)用時你必須寫成 superman.flyingEngine.fly() 而不是優(yōu)雅地寫成 superman.fly()

Mixins & Traits

生生不息勾徽,Mixin 繁榮

Mixins 和 Traits 的概念1由此引入。

  • 通過繼承统扳,你定義你的類是什么喘帚。例如每條 Dog一個 Animal
  • 通過 Traits咒钟,你定義你的類能做什么吹由。例如每個 Animal eat(),但是人類也可以吃朱嘴,而且異世奇人(Doctor Who)也能吃魚條和蛋撻溉知,甚至即使是位 Gallifreyan(既不是人類也不是動物)。

使用 Traits腕够,重要的不是「是什么」,而是能「做什么」舌劳。

繼承描述了一個對象是什么帚湘,而 Traits 描述了這個對象能做什么。

最棒的事情就是一個類可以選用多個 Traits 來做多個事情甚淡,而這個類還只是一種事物(只從一個父類繼承)大诸。

那么如何應用到 Swift 中呢捅厂?

有默認實現(xiàn)的協(xié)議

Swift 2.0 中定義一個協(xié)議(Protocol)的時候,還可以使用這個協(xié)議的擴展(Extension)給它的部分或是所有的方法做默認實現(xiàn)资柔”捍看上去是這樣的:

 func fly()
}

extension Flyer {
 func fly() {
 print("I believe I can flyyyyy ?")
 }
}

有了上面的代碼,當你創(chuàng)建一個遵從 Flyer 協(xié)議的類或者是結(jié)構(gòu)體時贿堰,就能很順利地獲得 fly() 方法辙芍!

這只是一個默認的實現(xiàn)方式。因此你可以在需要的時候不受約束地重新定義這個方法羹与;如果不重新定義的話故硅,會使用你默認的那個方法。

 // 這里我們沒有實現(xiàn) fly() 方法纵搁,因此能夠聽到 Clark 唱歌
}

class IronMan: Flyer {
 // 如果需要我們也可以給出單獨的實現(xiàn)
 func fly() {
 thrusters.start()
 }
}

對于很多事情來說吃衅,協(xié)議的默認實現(xiàn)這個特性非常的有用。其中一種自然就是如你所想的那樣腾誉,把「Traits」概念引入到了 Swift 中徘层。

一種身份,多種能力

Traits 很贊的一點就是它們并不依賴于使用到它們的對象本身的身份利职。Traits 并不關(guān)心類是什么趣效,亦或是類是從哪里繼承的:Traits 僅僅在類上定義了一些函數(shù)。

這就解決了我們的問題:異世奇人(Doctor Who)可以既是一位時間旅行者眼耀,同時還是一個外星人英支;而愛默·布朗博士(Dr Emmett Brown)既是一位時間旅行者,同時還屬于人類哮伟;鋼鐵俠(Iron Man)是一個能飛的人干花,而超人(Superman)是一個能飛的外星人。

你是什么并不限制你能夠做什么

現(xiàn)在我們利用 Traits 的優(yōu)點來實現(xiàn)一下我們的模板類楞黄。

首先定義不同的 Traits:

protocol Flyer {
 func fly()
}
protocol TimeTraveler {
 var currentDate: NSDate { get set }
 mutating func travelTo(date: NSDate)
}

隨后給它們一些默認的實現(xiàn):

extension Flyer {
 func fly() {
 print("I believe I can flyyyyy ?")
 }
}

extension TimeTraveler {
 mutating func travelTo(date: NSDate) {
 currentDate = date
 }
}

在這點上池凄,我們還是用繼承去定義我們英雄角色的身份(他們是什么),先定義一些父類:

|

class Character {
 var name: String
 init(name: String) {
 self.name = name
 }
}

class Human: Character {
 var countryOfOrigin: String?
 init(name: String, countryOfOrigin: String? = nil) {
 self.countryOfOrigin = countryOfOrigin
 super.init(name: name)
 }
}

class Alien: Character {
 let species: String
 init(name: String, species: String) {
 self.species = species
 super.init(name: name)
 }
}

現(xiàn)在我們就能通過他們的身份(通過繼承)和能力(Traits/協(xié)議遵循)來定義英雄角色了:

class TimeLord: Alien, TimeTraveler {
 var currentDate = NSDate()
 init() {
 super.init(name: "I'm the Doctor", species: "Gallifreyan")
 }
}

class DocEmmettBrown: Human, TimeTraveler {
 var currentDate = NSDate()
 init() {
 super.init(name: "Emmett Brown", countryOfOrigin: "USA")
 }
}

class Superman: Alien, Flyer {
 init() {
 super.init(name: "Clark Kent", species: "Kryptonian")
 }
}

class IronMan: Human, Flyer {
 init() {
 super.init(name: "Tony Stark", countryOfOrigin: "USA")
 }
}

現(xiàn)在 SupermanIronMan 都使用了相同的 fly() 實現(xiàn)鬼廓,即使他們分別繼承自不同的父類(一個繼承自 Alien肿仑,另一個繼承自 Human)。而且這兩位博士都知道怎么做時間旅行了碎税,即使一個是人類尤慰,另外一個來自 Gallifrey 星。

let tony = IronMan()
tony.fly() // 輸出 "I believe I can flyyyyy ?"
tony.name  // 返回 "Tony Stark"

let clark = Superman()
clark.fly() // 輸出 "I believe I can flyyyyy ?"
clark.species  // 返回 "Kryptonian"

var docBrown = DocEmmettBrown()
docBrown.travelTo(NSDate(timeIntervalSince1970: 499161600))
docBrown.name // "Emmett Brown"
docBrown.countryOfOrigin // "USA"
docBrown.currentDate // Oct 26, 1985, 9:00 AM

var doctorWho = TimeLord()
doctorWho.travelTo(NSDate(timeIntervalSince1970: 1303484520))
doctorWho.species // "Gallifreyan"
doctorWho.currentDate // Apr 22, 2011, 5:02 PM

時空大冒險

現(xiàn)在我們引入一個新的空間穿越的能力/trait:

protocol SpaceTraveler {
 func travelTo(location: String)
}

并給它一個默認的實現(xiàn):

extension SpaceTraveler {
 func travelTo(location: String) {
 print("Let's go to \(location)!")
 }
}

我們可以使用 Swift 的擴展(Extension)方式讓現(xiàn)有的一個類遵循一個協(xié)議雷蹂,把這些能力加到我們定義的角色身上去伟端。如果忽略掉鋼鐵俠之前跑到紐約城上面隨后短暫飛到太空中去的那次情景,那只有博士和超人是真正能做空間穿越的:

extension TimeLord: SpaceTraveler {}
extension Superman: SpaceTraveler {}

天哪责蝠!

沒錯党巾,這就是給已有類添加能力/trait 僅需的步驟!就這樣霜医,他們可以 travelTo() 任何的地方了齿拂!很簡潔,是吧肴敛?

doctorWho.travelTo("Trenzalore") // prints "Let's go to Trenzalore!"

邀請更多的人來參加這場聚會署海!

現(xiàn)在我們再讓更多的人加入進來吧:

// 來吧,Pond值朋!
let amy = Human(name: "Amelia Pond", countryOfOrigin: "UK")
// 該死叹侄,她是一個時間和空間旅行者,但是卻不是 TimeLord昨登!

class Astraunaut: Human, SpaceTraveler {}
let neilArmstrong = Astraunaut(name: "Neil Armstro
ng", countryOfOrigin: "USA")
let laika = Astraunaut(name: "La?ka", countryOfOrigin: "Russia")
// 等等趾代,Le?ka 是一只狗,不是嗎丰辣?

class MilleniumFalconPilot: Human, SpaceTraveler {}
let hanSolo = MilleniumFalconPilot(name: "Han Solo")
let chewbacca = MilleniumFalconPilot(name: "Chewie")
// 等等撒强,MilleniumFalconPilot 不該定義成「人類」吧!

class Spock: Alien, SpaceTraveler {
 init() {
 super.init(name: "Spock", species: "Vulcan")
 // 并不是 100% 正確
 }
}

Huston笙什,我們有麻煩了(譯注:原文 “Huston, we have a problem here”飘哨,是星際迷航中的梗)。Laika 不是一個人琐凭,Chewie 也不是芽隆,Spock 算半個人、半個瓦肯(Vulcan)人统屈,所以上面的代碼定義錯的離譜胚吁!

你看出來什么問題了么?我們又一次被繼承擺了一道愁憔,理所應當?shù)卣J為 HumanAlien是身份腕扶。在這里一些類必須屬于某種類型,或是必須繼承自某個父類吨掌,而實際情況中不總是這樣半抱,尤其對科幻故事來說。

這也是為什么要在 Swift 中使用協(xié)議膜宋,以及協(xié)議的默認擴展窿侈。這能夠幫助我們把因使用繼承而強加到類上的這些限制移除。

如果 HumanAlien 不是而是協(xié)議秋茫,那就會有很多的好處:

  • 我們可以定義一個 MilleniumFalconPilot 類型史简,不必讓它是一個 Human ,這樣就可以讓 Chewie 駕駛它了学辱;
  • 我們可以把 La?ka 定義成一個 Astronaut乘瓤,即使她不是人類;
  • 我們可以將 Spock 定義成 HumanAlien 的結(jié)合體策泣;
  • 我們甚至可以在這個例子中完全摒棄繼承衙傀,并將我們的類型從類(Classes)轉(zhuǎn)換成結(jié)構(gòu)體(Structs)結(jié)構(gòu)體不支持繼承萨咕,但可以遵循你想要遵循的協(xié)議统抬,想遵循多少協(xié)議就能遵循多少協(xié)議!

無處不在的協(xié)議危队!

因此聪建,我們的一個解決方案是徹底棄用繼承,將所有的東西都變成協(xié)議茫陆。畢竟我們不在乎我們的角色是什么金麸,能夠定義英雄本身的是他們擁有的能力

終結(jié)掉繼承挥下!

我在這里附上了一個可下載的 Swift Playground 文件,包含這篇文章里的所有代碼桨醋,并在 Playground 的第二頁放上了一個全部用協(xié)議和結(jié)構(gòu)體的解決方案棚瘟,完全不用繼承∠沧睿快去看看吧偎蘸!

這當然并不意味著你必須不惜一切代價放棄對繼承的使用(別聽那個 Dalek 講太多,機器人畢竟沒感情的??)瞬内。繼承依然有用迷雪,而且依然有意義——很符合邏輯的一個說法就是 UILabelUIView 的一個子類。但我們提供的方法能讓你能感受到 Mixins 和協(xié)議帶給你的不同體驗遂鹊。

小結(jié)

實踐 Swift 的時候振乏,你會意識到它實質(zhì)上是一個面向協(xié)議的語言(Protocols-Oriented language),而且在 Swift 中使用協(xié)議和在 Objective-C 中使用相比更加常見和有效秉扑。畢竟慧邮,那些類似于 EquatableCustomStringConvertible 的協(xié)議以及 Swift 標準庫中其它所有以 -able 結(jié)尾的協(xié)議都可以被看做是 Mixins舟陆!

有了 Swift 的協(xié)議和協(xié)議的默認實現(xiàn)误澳,你就能實現(xiàn) Mixins 和 Traits,而且你還可以實現(xiàn)類似于抽象類2以及更多的一些東西秦躯,這讓你的代碼變得更加靈活忆谓。

Mixins 和 Traits 的方式可以讓你描述你的類型能夠做什么,而不是描述它們是什么踱承。更重要的是倡缠,它們能夠為你的類型增加各種能力哨免。這就像購物那樣,無論你的類是從哪個父類繼承的(如果有)昙沦,你都能為它們選擇你想要它們具有的那些能力琢唾。

回到第一個例子,你可以創(chuàng)建一個 BurgerMenuManager 協(xié)議且該協(xié)議有一個默認實現(xiàn)盾饮,然后可以簡單地將 View Controllers(不論是 UIViewController采桃,UITableViewController 還是其他的類)都遵循這個協(xié)議,它們都能自動獲得 BurgerMenuManager 所具有的能力和特性丘损,你也根本不用去為父類 UIViewController 操心普办!

我不想離開

關(guān)于協(xié)議擴展還有很多要說的,我還想在文章中繼續(xù)告訴你關(guān)于它更多的事情徘钥,因為它能夠通過很多方式提高你的代碼質(zhì)量衔蹲。嘿,但是吏饿,這篇文章已經(jīng)挺長的了踪危,同時也為以后的博客文章留一些空間吧,希望你到時還會再來看猪落!

與此同時贞远,生生不息,繁榮昌盛笨忌,杰羅尼莫(譯注:跳傘時老兵鼓勵新兵的一句話)蓝仲!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市官疲,隨后出現(xiàn)的幾起案子袱结,更是在濱河造成了極大的恐慌,老刑警劉巖途凫,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件垢夹,死亡現(xiàn)場離奇詭異,居然都是意外死亡维费,警方通過查閱死者的電腦和手機果元,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來犀盟,“玉大人而晒,你說我怎么就攤上這事≡某耄” “怎么了倡怎?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我监署,道長颤专,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任钠乏,我火速辦了婚禮血公,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘缓熟。我一直安慰自己,他們只是感情好摔笤,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布够滑。 她就那樣靜靜地躺著,像睡著了一般吕世。 火紅的嫁衣襯著肌膚如雪彰触。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天命辖,我揣著相機與錄音况毅,去河邊找鬼。 笑死尔艇,一個胖子當著我的面吹牛尔许,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播终娃,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼味廊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了棠耕?” 一聲冷哼從身側(cè)響起余佛,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎窍荧,沒想到半個月后辉巡,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡蕊退,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年郊楣,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片咕痛。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡痢甘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出茉贡,到底是詐尸還是另有隱情塞栅,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站放椰,受9級特大地震影響作烟,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜砾医,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一拿撩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧如蚜,春花似錦压恒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至撬呢,卻和暖如春伦吠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背魂拦。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工毛仪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人芯勘。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓箱靴,卻偏偏與公主長得像,于是被迫代替她去往敵國和親荷愕。 傳聞我的和親對象是個殘疾皇子刨晴,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348

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