作者:Olivier Halligon累榜,原文鏈接颈墅,原文日期:2015-11-08
譯者:ray16897188章母;校對(duì):Cee存哲;定稿:千葉知風(fēng)
譯者注:Mixin 和 [Trait](https://en.wikipedia.org/wiki/Trait_(computer_programming) 是面向?qū)ο缶幊陶Z(yǔ)言中的術(shù)語(yǔ)因宇,本文中作者并未明確指出兩者之間的區(qū)別七婴。這兩個(gè)單詞在本譯文中也不做翻譯。
從面向?qū)ο蟮木幊陶Z(yǔ)言的角度來(lái)說察滑,繼承(Inheritence)總被用來(lái)在多個(gè)類之間共享代碼打厘。但這并不總是一個(gè)最佳的解決方案,而且它本身還有些問題贺辰。在今天寫的這篇文章中户盯,我們會(huì)看到 Swift 中的協(xié)議擴(kuò)展(Protocol Extensions),并將其以「Mixins」的形式去使用是怎樣解決這個(gè)問題的饲化。
你可以從這里下載包含本篇文章所有代碼的 Swift Playground莽鸭。
繼承本身存在的問題
假設(shè)你有個(gè) app,里面有很多包含相同行為的 UIViewController
類吃靠,例如它們都有漢堡菜單硫眨。你當(dāng)然不想在 app 中的每一個(gè) View Controller 里都反復(fù)實(shí)現(xiàn)這個(gè)漢堡菜單的邏輯(例如設(shè)置 leftBarButtonItem
按鈕,點(diǎn)擊這個(gè)按鈕時(shí)打開或者關(guān)閉這個(gè)菜單巢块,等等)礁阁。
解決方案很簡(jiǎn)單,你只需要?jiǎng)?chuàng)建一個(gè)負(fù)責(zé)實(shí)現(xiàn)所有特定行為族奢、而且是 UIViewController
的子類 CommonViewController
氮兵。然后讓你所有的 ViewController 都直接繼承 CommonViewController
而不是 UIViewController
就可以了,沒錯(cuò)吧歹鱼?通過使用這種方式,這些類都繼承了父類的方法卜高,且具有了相同的行為弥姻,你也不用每次重復(fù)實(shí)現(xiàn)這些東西了。
class CommonViewController: UIViewController {
func setupBurgerMenu() { … }
func onBurgerMenuTapped() { … }
var burgerMenuIsOpen: Bool {
didSet { … }
}
}
class MyViewController: CommonViewController {
func viewDidLoad() {
super.viewDidLoad()
setupBurgerMenu()
}
}
但在隨后的開發(fā)階段掺涛,你會(huì)意識(shí)到自己需要一個(gè) UITableViewController
或者一個(gè) UICollectionViewController
……暈死庭敦,CommonViewController
不能用了,因?yàn)樗抢^承自 UIViewController
而不是 UITableViewController
薪缆!
你會(huì)怎么做秧廉,是實(shí)現(xiàn)和 CommonViewController
一樣的事情卻繼承于 UITableViewController
的 CommonTableViewController
嗎?這會(huì)產(chǎn)生很多重復(fù)的代碼拣帽,而且是個(gè)十分糟糕的設(shè)計(jì)哦疼电。
組合(Composition)是救命稻草
誠(chéng)然,解決這個(gè)問題减拭,有句具有代表性并且正確的話是這么說的:
多用組合蔽豺,少用繼承。
這意味著我們不使用繼承的方式拧粪,而是讓我們的 UIViewController
包含一些提供相應(yīng)行為的內(nèi)部類(Inner class)修陡。
在這個(gè)例子中沧侥,我們可以假定 BurgerMenuManager
類能提供創(chuàng)建漢堡菜單圖標(biāo)、以及與這些圖標(biāo)交互邏輯的所有必要的方法魄鸦。那些各式各樣的 UIViewController
就會(huì)有一個(gè) BurgerMenuManager
類型的屬性宴杀,可以用來(lái)與漢堡餐單做交互。
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()
}
}
然而你能看出來(lái)這種解決方案會(huì)變得很臃腫拾因。每次你都得去明確引用那個(gè)中間對(duì)象 menuManager
旺罢。
多繼承(Multiple inheritance)
繼承的另一個(gè)問題就是很多面向?qū)ο蟮木幊陶Z(yǔ)言都不支持多繼承(這兒有個(gè)很好的解釋,是關(guān)于菱形缺陷(Diamond problem)的)盾致。
這就意味著一個(gè)類不能繼承自多個(gè)父類主经。
假如說你要?jiǎng)?chuàng)建一些科幻小說中的人物的對(duì)象模型。顯然庭惜,你得展現(xiàn)出 DocEmmettBrown
罩驻,DoctorWho
,TimeLord
护赊,IronMan
還有 Superman
的能力……這些角色的相互關(guān)系是什么惠遏?有些能時(shí)間旅行,有些能空間穿越骏啰,還有些兩種能力都會(huì)节吮;有些能飛,而有些不能飛判耕;有些是人類透绩,而有些不是……
IronMan
和 Superman
這個(gè)兩個(gè)類都能飛,于是我們就會(huì)設(shè)想有個(gè) Flyer
類能提供一個(gè)實(shí)現(xiàn) fly()
的方法壁熄。但是 IronMan
和 DocEmmettBrown
都是人類帚豪,我們還會(huì)設(shè)想要有個(gè) Human
父類;而 Superman
和 TimeLord
又得是 Alien
的子類草丧。哦狸臣,等會(huì)兒…… 那 IronMan
得同時(shí)繼承 Flyer
和 Human
兩個(gè)類嗎?這在 Swift 中是不可能的實(shí)現(xiàn)的(在很多其他的面向?qū)ο蟮恼Z(yǔ)言中也不能這么實(shí)現(xiàn))昌执。
我們應(yīng)該從所有父類中選擇出符合子類屬性最好的一個(gè)么烛亦?但是假如我們讓 IronMan
繼承 Human
,那么怎么去實(shí)現(xiàn) fly()
這個(gè)方法懂拾?很顯然我們不能在 Human
這個(gè)類中實(shí)現(xiàn)煤禽,因?yàn)椴⒉皇敲總€(gè)人都會(huì)飛,但是 Superman
卻需要這個(gè)方法岖赋,然而我們并不想重復(fù)寫兩次呜师。
所以,我們?cè)谶@里會(huì)使用組合(Composition)方法贾节,讓 var flyingEngine: Flyer
成為 Superman
類中的一個(gè)屬性汁汗。
但是調(diào)用時(shí)你必須寫成 superman.flyingEngine.fly()
而不是優(yōu)雅地寫成 superman.fly()
衷畦。
Mixins & Traits
Mixins 和 Traits 的概念<sup id="fnref1"><a href="#fn1" rel="footnote">1</a>由此引入祈争。
- 通過繼承,你定義你的類是什么角寸。例如每條
Dog
都是一個(gè)Animal
菩混。 - 通過 Traits,你定義你的類能做什么扁藕。例如每個(gè)
Animal
都能eat()
沮峡,但是人類也可以吃,而且異世奇人(Doctor Who)也能吃魚條和蛋撻亿柑,甚至即使是位 Gallifreyan(既不是人類也不是動(dòng)物)邢疙。
使用 Traits,重要的不是「是什么」望薄,而是能「做什么」疟游。
繼承描述了一個(gè)對(duì)象是什么,而 Traits 描述了這個(gè)對(duì)象能做什么痕支。
最棒的事情就是一個(gè)類可以選用多個(gè) Traits
來(lái)做多個(gè)事情颁虐,而這個(gè)類還只是一種事物(只從一個(gè)父類繼承)智末。
那么如何應(yīng)用到 Swift 中呢虾标?
有默認(rèn)實(shí)現(xiàn)的協(xié)議
Swift 2.0 中定義一個(gè)協(xié)議(Protocol)
的時(shí)候,還可以使用這個(gè)協(xié)議的擴(kuò)展(Extension)
給它的部分或是所有的方法做默認(rèn)實(shí)現(xiàn)南吮』ㄋ唬看上去是這樣的:
protocol Flyer {
func fly()
}
extension Flyer {
func fly() {
print("I believe I can flyyyyy ?")
}
}
有了上面的代碼板熊,當(dāng)你創(chuàng)建一個(gè)遵從 Flyer
協(xié)議的類或者是結(jié)構(gòu)體時(shí),就能很順利地獲得 fly()
方法察绷!
這只是一個(gè)默認(rèn)的實(shí)現(xiàn)方式。因此你可以在需要的時(shí)候不受約束地重新定義這個(gè)方法津辩;如果不重新定義的話拆撼,會(huì)使用你默認(rèn)的那個(gè)方法。
class SuperMan: Flyer {
// 這里我們沒有實(shí)現(xiàn) fly() 方法喘沿,因此能夠聽到 Clark 唱歌
}
class IronMan: Flyer {
// 如果需要我們也可以給出單獨(dú)的實(shí)現(xiàn)
func fly() {
thrusters.start()
}
}
對(duì)于很多事情來(lái)說闸度,協(xié)議的默認(rèn)實(shí)現(xiàn)這個(gè)特性非常的有用。其中一種自然就是如你所想的那樣蚜印,把「Traits」概念引入到了 Swift 中莺禁。
一種身份,多種能力
Traits 很贊的一點(diǎn)就是它們并不依賴于使用到它們的對(duì)象本身的身份窄赋。Traits 并不關(guān)心類是什么哟冬,亦或是類是從哪里繼承的:Traits 僅僅在類上定義了一些函數(shù)楼熄。
這就解決了我們的問題:異世奇人(Doctor Who)可以既是一位時(shí)間旅行者,同時(shí)還是一個(gè)外星人浩峡;而愛默·布朗博士(Dr Emmett Brown)既是一位時(shí)間旅行者可岂,同時(shí)還屬于人類;鋼鐵俠(Iron Man)是一個(gè)能飛的人翰灾,而超人(Superman)是一個(gè)能飛的外星人缕粹。
你是什么并不限制你能夠做什么
現(xiàn)在我們利用 Traits 的優(yōu)點(diǎn)來(lái)實(shí)現(xiàn)一下我們的模板類。
首先定義不同的 Traits:
protocol Flyer {
func fly()
}
protocol TimeTraveler {
var currentDate: NSDate { get set }
mutating func travelTo(date: NSDate)
}
隨后給它們一些默認(rèn)的實(shí)現(xiàn):
extension Flyer {
func fly() {
print("I believe I can flyyyyy ?")
}
}
extension TimeTraveler {
mutating func travelTo(date: NSDate) {
currentDate = date
}
}
在這點(diǎn)上纸淮,我們還是用繼承去定義我們英雄角色的身份(他們是什么)平斩,先定義一些父類:
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é)議遵循)來(lái)定義英雄角色了:
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)在 Superman
和 IronMan
都使用了相同的 fly()
實(shí)現(xiàn),即使他們分別繼承自不同的父類(一個(gè)繼承自 Alien
咽块,另一個(gè)繼承自 Human
)绘面。而且這兩位博士都知道怎么做時(shí)間旅行了,即使一個(gè)是人類糜芳,另外一個(gè)來(lái)自 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
時(shí)空大冒險(xiǎn)
現(xiàn)在我們引入一個(gè)新的空間穿越的能力/trait:
protocol SpaceTraveler {
func travelTo(location: String)
}
并給它一個(gè)默認(rèn)的實(shí)現(xiàn):
extension SpaceTraveler {
func travelTo(location: String) {
print("Let's go to \(location)!")
}
}
我們可以使用 Swift 的擴(kuò)展(Extension)
方式讓現(xiàn)有的一個(gè)類遵循一個(gè)協(xié)議,把這些能力加到我們定義的角色身上去峭竣。如果忽略掉鋼鐵俠之前跑到紐約城上面隨后短暫飛到太空中去的那次情景塘辅,那只有博士和超人是真正能做空間穿越的:
extension TimeLord: SpaceTraveler {}
extension Superman: SpaceTraveler {}
沒錯(cuò)扣墩,這就是給已有類添加能力/trait 僅需的步驟!就這樣扛吞,他們可以 travelTo()
任何的地方了呻惕!很簡(jiǎn)潔,是吧滥比?
doctorWho.travelTo("Trenzalore") // prints "Let's go to Trenzalore!"
邀請(qǐng)更多的人來(lái)參加這場(chǎng)聚會(huì)亚脆!
現(xiàn)在我們?cè)僮尭嗟娜思尤脒M(jìn)來(lái)吧:
// 來(lái)吧,Pond盲泛!
let amy = Human(name: "Amelia Pond", countryOfOrigin: "UK")
// 該死濒持,她是一個(gè)時(shí)間和空間旅行者,但是卻不是 TimeLord寺滚!
class Astraunaut: Human, SpaceTraveler {}
let neilArmstrong = Astraunaut(name: "Neil Armstrong", 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 不是一個(gè)人,Chewie 也不是纺阔,Spock 算半個(gè)人瘸彤、半個(gè)瓦肯(Vulcan)人,所以上面的代碼定義錯(cuò)的離譜笛钝!
你看出來(lái)什么問題了么质况?我們又一次被繼承擺了一道,理所應(yīng)當(dāng)?shù)卣J(rèn)為 Human
和 Alien
是身份玻靡。在這里一些類必須屬于某種類型结榄,或是必須繼承自某個(gè)父類,而實(shí)際情況中不總是這樣囤捻,尤其對(duì)科幻故事來(lái)說臼朗。
這也是為什么要在 Swift 中使用協(xié)議,以及協(xié)議的默認(rèn)擴(kuò)展蝎土。這能夠幫助我們把因使用繼承而強(qiáng)加到類上的這些限制移除视哑。
如果 Human
和 Alien
不是類
而是協(xié)議
,那就會(huì)有很多的好處:
- 我們可以定義一個(gè)
MilleniumFalconPilot
類型誊涯,不必讓它是一個(gè)Human
挡毅,這樣就可以讓 Chewie 駕駛它了; - 我們可以把 La?ka 定義成一個(gè)
Astronaut
暴构,即使她不是人類跪呈; - 我們可以將
Spock
定義成Human
和Alien
的結(jié)合體; - 我們甚至可以在這個(gè)例子中完全摒棄繼承取逾,并將我們的類型從
類(Classes)
轉(zhuǎn)換成結(jié)構(gòu)體(Structs)
耗绿。結(jié)構(gòu)體
不支持繼承,但可以遵循你想要遵循的協(xié)議砾隅,想遵循多少協(xié)議就能遵循多少協(xié)議误阻!
無(wú)處不在的協(xié)議!
因此晴埂,我們的一個(gè)解決方案是徹底棄用繼承究反,將所有的東西都變成協(xié)議。畢竟我們不在乎我們的角色是什么邑时,能夠定義英雄本身的是他們擁有的能力!
我在這里附上了一個(gè)可下載的 Swift Playground 文件,包含這篇文章里的所有代碼,并在 Playground 的第二頁(yè)放上了一個(gè)全部用協(xié)議和結(jié)構(gòu)體的解決方案浅浮,完全不用繼承沫浆。快去看看吧滚秩!
這當(dāng)然并不意味著你必須不惜一切代價(jià)放棄對(duì)繼承的使用(別聽那個(gè) Dalek 講太多专执,機(jī)器人畢竟沒感情的??)。繼承依然有用郁油,而且依然有意義——很符合邏輯的一個(gè)說法就是 UILabel
是 UIView
的一個(gè)子類本股。但我們提供的方法能讓你能感受到 Mixins 和協(xié)議帶給你的不同體驗(yàn)。
小結(jié)
實(shí)踐 Swift 的時(shí)候桐腌,你會(huì)意識(shí)到它實(shí)質(zhì)上是一個(gè)面向協(xié)議的語(yǔ)言(Protocols-Oriented language)拄显,而且在 Swift 中使用協(xié)議和在 Objective-C 中使用相比更加常見和有效。畢竟案站,那些類似于 Equatable
躬审,CustomStringConvertible
的協(xié)議以及 Swift 標(biāo)準(zhǔn)庫(kù)中其它所有以 -able
結(jié)尾的協(xié)議都可以被看做是 Mixins!
有了 Swift 的協(xié)議和協(xié)議的默認(rèn)實(shí)現(xiàn)蟆盐,你就能實(shí)現(xiàn) Mixins 和 Traits承边,而且你還可以實(shí)現(xiàn)類似于抽象類<sup id="fnref2"><a href="#fn2" rel="footnote">2</a>以及更多的一些東西,這讓你的代碼變得更加靈活石挂。
Mixins 和 Traits 的方式可以讓你描述你的類型能夠做什么博助,而不是描述它們是什么。更重要的是誊稚,它們能夠?yàn)槟愕念愋驮黾痈鞣N能力翔始。這就像購(gòu)物那樣,無(wú)論你的類是從哪個(gè)父類繼承的(如果有)里伯,你都能為它們選擇你想要它們具有的那些能力城瞎。
回到第一個(gè)例子,你可以創(chuàng)建一個(gè) BurgerMenuManager 協(xié)議
且該協(xié)議有一個(gè)默認(rèn)實(shí)現(xiàn)疾瓮,然后可以簡(jiǎn)單地將 View Controllers(不論是 UIViewController
脖镀,UITableViewController
還是其他的類)都遵循這個(gè)協(xié)議,它們都能自動(dòng)獲得 BurgerMenuManager
所具有的能力和特性狼电,你也根本不用去為父類 UIViewController
操心蜒灰!
關(guān)于協(xié)議擴(kuò)展還有很多要說的,我還想在文章中繼續(xù)告訴你關(guān)于它更多的事情肩碟,因?yàn)樗軌蛲ㄟ^很多方式提高你的代碼質(zhì)量强窖。嘿,但是削祈,這篇文章已經(jīng)挺長(zhǎng)的了翅溺,同時(shí)也為以后的博客文章留一些空間吧脑漫,希望你到時(shí)還會(huì)再來(lái)看!
與此同時(shí)咙崎,生生不息优幸,繁榮昌盛,杰羅尼莫(譯注:跳傘時(shí)老兵鼓勵(lì)新兵的一句話)褪猛!
<a id="fn1" href="#fnref1" rev="footnote">1.我不會(huì)深入去講 Mixin 和 Traits 這兩個(gè)概念之間的區(qū)別网杆。由于這兩個(gè)詞的意思很接近,為簡(jiǎn)單起見伊滋,在本篇文章中它倆可以互相替換使用碳却。</a>
<a id="fn2" href="#fnref2" rev="footnote">2.在以后的博文中會(huì)作為一個(gè)專題去講解。</a>
本文由 SwiftGG 翻譯組翻譯新啼,已經(jīng)獲得作者翻譯授權(quán)追城,最新文章請(qǐng)?jiān)L問 http://swift.gg。