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

相关文章
相关标签/搜索