Mixins 比繼承更好

做者:Olivier Halligon,原文連接,原文日期:2015-11-08
譯者:ray16897188;校對:Cee;定稿:千葉知風git

譯者注:MixinTrait 是面向對象編程語言中的術語,本文中做者並未明確指出二者之間的區別。這兩個單詞在本譯文中也不作翻譯。github

從面向對象的編程語言的角度來講,繼承(Inheritence)總被用來在多個類之間共享代碼。但這並不老是一個最佳的解決方案,並且它自己還有些問題。在今天寫的這篇文章中,咱們會看到 Swift 中的協議擴展(Protocol Extensions),並將其以「Mixins」的形式去使用是怎樣解決這個問題的。編程

你能夠從這裏下載包含本篇文章全部代碼的 Swift Playgroundswift

繼承自己存在的問題

假設你有個 app,裏面有不少包含相同行爲的 UIViewController 類,例如它們都有漢堡菜單。你固然不想在 app 中的每個 View Controller 裏都反覆實現這個漢堡菜單的邏輯(例如設置 leftBarButtonItem 按鈕,點擊這個按鈕時打開或者關閉這個菜單,等等)。app

解決方案很簡單,你只須要建立一個負責實現全部特定行爲、並且是 UIViewController 的子類 CommonViewController。而後讓你全部的 ViewController 都直接繼承 CommonViewController 而不是 UIViewController 就能夠了,沒錯吧?經過使用這種方式,這些類都繼承了父類的方法,且具備了相同的行爲,你也不用每次重複實現這些東西了。編程語言

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

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

但在隨後的開發階段,你會意識到本身須要一個 UITableViewController 或者一個 UICollectionViewController……暈死,CommonViewController 不能用了,由於它是繼承自 UIViewController 而不是 UITableViewController函數

你會怎麼作,是實現和 CommonViewController 同樣的事情卻繼承於 UITableViewControllerCommonTableViewController 嗎?這會產生不少重複的代碼,並且是個十分糟糕的設計哦。ui

組合(Composition)是救命稻草

誠然,解決這個問題,有句具備表明性而且正確的話是這麼說的:spa

多用組合,少用繼承。翻譯

這意味着咱們不使用繼承的方式,而是讓咱們的 UIViewController 包含一些提供相應行爲的內部類(Inner class)。

在這個例子中,咱們能夠假定 BurgerMenuManager 類能提供建立漢堡菜單圖標、以及與這些圖標交互邏輯的全部必要的方法。那些各式各樣的 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)

繼承的另外一個問題就是不少面向對象的編程語言都不支持多繼承(這兒有個很好的解釋,是關於菱形缺陷(Diamond problem)的)。

這就意味着一個類不能繼承自多個父類。

假如說你要建立一些科幻小說中的人物的對象模型。顯然,你得展示出 DocEmmettBrownDoctorWhoTimeLordIronMan 還有 Superman 的能力……這些角色的相互關係是什麼?有些能時間旅行,有些能空間穿越,還有些兩種能力都會;有些能飛,而有些不能飛;有些是人類,而有些不是……

IronManSuperman 這個兩個類都能飛,因而咱們就會設想有個 Flyer 類能提供一個實現 fly() 的方法。可是 IronManDocEmmettBrown 都是人類,咱們還會設想要有個 Human 父類;而 SupermanTimeLord 又得是 Alien 的子類。哦,等會兒…… 那 IronMan 得同時繼承 FlyerHuman 兩個類嗎?這在 Swift 中是不可能的實現的(在不少其餘的面向對象的語言中也不能這麼實現)。

咱們應該從全部父類中選擇出符合子類屬性最好的一個麼?可是假如咱們讓 IronMan 繼承 Human,那麼怎麼去實現 fly() 這個方法?很顯然咱們不能在 Human 這個類中實現,由於並非每一個人都會飛,可是 Superman 卻須要這個方法,然而咱們並不想重複寫兩次。

因此,咱們在這裏會使用組合(Composition)方法,讓 var flyingEngine: Flyer 成爲 Superman 類中的一個屬性。

可是調用時你必須寫成 superman.flyingEngine.fly() 而不是優雅地寫成 superman.fly()

Mixins & Traits

生生不息,Mixin 繁榮

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

  • 經過繼承,你定義你的類是什麼。例如每條 Dog一個 Animal

  • 經過 Traits,你定義你的類能作什麼。例如每一個 Animal eat(),可是人類也能夠吃,並且異世奇人(Doctor Who)也能吃魚條和蛋撻,甚至即便是位 Gallifreyan(既不是人類也不是動物)。

使用 Traits,重要的不是「是什麼」,而是能「作什麼」。

繼承描述了一個對象是什麼,而 Traits 描述了這個對象能作什麼。

最棒的事情就是一個類能夠選用多個 Traits 來作多個事情,而這個類還只是一種事物(只從一個父類繼承)。

那麼如何應用到 Swift 中呢?

有默認實現的協議

Swift 2.0 中定義一個協議(Protocol)的時候,還可使用這個協議的擴展(Extension)給它的部分或是全部的方法作默認實現。看上去是這樣的:

protocol Flyer {
  func fly()
}

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

有了上面的代碼,當你建立一個聽從 Flyer 協議的類或者是結構體時,就能很順利地得到 fly() 方法!

這只是一個默認的實現方式。所以你能夠在須要的時候不受約束地從新定義這個方法;若是不從新定義的話,會使用你默認的那個方法。

class SuperMan: Flyer {
  // 這裏咱們沒有實現 fly() 方法,所以可以聽到 Clark 唱歌
}

class IronMan: Flyer {
  // 若是須要咱們也能夠給出單獨的實現
  func fly() {
    thrusters.start()
  }
}

對於不少事情來講,協議的默認實現這個特性很是的有用。其中一種天然就是如你所想的那樣,把「Traits」概念引入到了 Swift 中。

一種身份,多種能力

Traits 很讚的一點就是它們並不依賴於使用到它們的對象自己的身份。Traits 並不關心類是什麼,亦或是類是從哪裏繼承的:Traits 僅僅在類上定義了一些函數。

這就解決了咱們的問題:異世奇人(Doctor Who)能夠既是一位時間旅行者,同時仍是一個外星人;而愛默·布朗博士(Dr Emmett Brown)既是一位時間旅行者,同時還屬於人類;鋼鐵俠(Iron Man)是一個能飛的人,而超人(Superman)是一個能飛的外星人。

你是什麼並不限制你可以作什麼

如今咱們利用 Traits 的優勢來實現一下咱們的模板類。

首先定義不一樣的 Traits:

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

隨後給它們一些默認的實現:

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

如今咱們就能經過他們的身份(經過繼承)和能力(Traits/協議遵循)來定義英雄角色了:

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

如今 SupermanIronMan 都使用了相同的 fly() 實現,即便他們分別繼承自不一樣的父類(一個繼承自 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

時空大冒險

如今咱們引入一個新的空間穿越的能力/trait:

protocol SpaceTraveler {
  func travelTo(location: String)
}

並給它一個默認的實現:

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

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

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

天哪!

沒錯,這就是給已有類添加能力/trait 僅需的步驟!就這樣,他們能夠 travelTo() 任何的地方了!很簡潔,是吧?

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

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

如今咱們再讓更多的人加入進來吧:

// 來吧,Pond!
let amy = Human(name: "Amelia Pond", countryOfOrigin: "UK")
// 該死,她是一個時間和空間旅行者,可是卻不是 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 不是一我的,Chewie 也不是,Spock 算半我的、半個瓦肯(Vulcan)人,因此上面的代碼定義錯的離譜!

你看出來什麼問題了麼?咱們又一次被繼承擺了一道,理所應當地認爲 HumanAlien 是身份。在這裏一些類必須屬於某種類型,或是必須繼承自某個父類,而實際狀況中不老是這樣,尤爲對科幻故事來講。

這也是爲何要在 Swift 中使用協議,以及協議的默認擴展。這可以幫助咱們把因使用繼承而強加到類上的這些限制移除。

若是 HumanAlien 不是而是協議,那就會有不少的好處:

  • 咱們能夠定義一個 MilleniumFalconPilot 類型,沒必要讓它是一個 Human ,這樣就可讓 Chewie 駕駛它了;

  • 咱們能夠把 Laïka 定義成一個 Astronaut,即便她不是人類;

  • 咱們能夠將 Spock 定義成 HumanAlien 的結合體;

  • 咱們甚至能夠在這個例子中徹底摒棄繼承,並將咱們的類型從類(Classes)轉換成結構體(Structs)結構體不支持繼承,但能夠遵循你想要遵循的協議,想遵循多少協議就能遵循多少協議!

無處不在的協議!

所以,咱們的一個解決方案是完全棄用繼承,將全部的東西都變成協議。畢竟咱們不在意咱們的角色是什麼,可以定義英雄自己的是他們擁有的能力

終結掉繼承!

我在這裏附上了一個可下載的 Swift Playground 文件,包含這篇文章裏的全部代碼,並在 Playground 的第二頁放上了一個所有用協議和結構體的解決方案,徹底不用繼承。快去看看吧!

這固然並不意味着你必須不惜一切代價放棄對繼承的使用(別聽那個 Dalek 講太多,機器人畢竟沒感情的?)。繼承依然有用,並且依然有意義——很符合邏輯的一個說法就是 UILabelUIView 的一個子類。但咱們提供的方法能讓你能感覺到 Mixins 和協議帶給你的不一樣體驗。

小結

實踐 Swift 的時候,你會意識到它實質上是一個面向協議的語言(Protocols-Oriented language),並且在 Swift 中使用協議和在 Objective-C 中使用相比更加常見和有效。畢竟,那些相似於 EquatableCustomStringConvertible 的協議以及 Swift 標準庫中其它全部以 -able 結尾的協議均可以被看作是 Mixins!

有了 Swift 的協議和協議的默認實現,你就能實現 Mixins 和 Traits,並且你還能夠實現相似於抽象類2以及更多的一些東西,這讓你的代碼變得更加靈活。

Mixins 和 Traits 的方式可讓你描述你的類型可以作什麼,而不是描述它們是什麼。更重要的是,它們可以爲你的類型增長各類能力。這就像購物那樣,不管你的類是從哪一個父類繼承的(若是有),你都能爲它們選擇你想要它們具備的那些能力

回到第一個例子,你能夠建立一個 BurgerMenuManager 協議且該協議有一個默認實現,而後能夠簡單地將 View Controllers(不管是 UIViewControllerUITableViewController 仍是其餘的類)都遵循這個協議,它們都能自動得到 BurgerMenuManager 所具備的能力和特性,你也根本不用去爲父類 UIViewController 操心!

我不想離開

關於協議擴展還有不少要說的,我還想在文章中繼續告訴你關於它更多的事情,由於它可以經過不少方式提升你的代碼質量。嘿,可是,這篇文章已經挺長的了,同時也爲之後的博客文章留一些空間吧,但願你到時還會再來看!

與此同時,生生不息,繁榮昌盛,傑羅尼莫(譯註:跳傘時老兵鼓勵新兵的一句話)!


1.我不會深刻去講 Mixin 和 Traits 這兩個概念之間的區別。因爲這兩個詞的意思很接近,爲簡單起見,在本篇文章中它倆能夠互相替換使用。
2.在之後的博文中會做爲一個專題去講解。

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg

相關文章
相關標籤/搜索