超越继承之路:协议混合

只要你学习过面向对象的语言比如 ObjC ,都知道继承的概念,他的一个用途是在多个类之间共享代码。但是这种解决方案存在一些问题。这篇文章我们来初探一下 Swift 的协议扩展,以及如何混合使用这些协议 - Mixins,英文原文地址

如果感觉太长了,读不下去,可以直接下载代码 Swift Playground Code

继承的问题

比如你有个 app,其中有大量的 UIViewController 类都要共享相同的行为,例如他们都有一个相同样式的汉堡菜单。你不想在每个 View Controllers 中都实现一遍『汉堡菜单』的逻辑(设置 leftBarButtonItem,按钮点击时打开/关闭菜单)

解决方法很简单,创建一个通用的 CommonViewController,继承自 UIViewController,然后实现所有的行为,接着让其他的 UIViewController 继承自这个 CommonViewController,而不是直接继承自 UIViewController。通过这种方式,这些 VC 将拥有这些相同的方法和行为,不需要再每次都自己实现一遍了。

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

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

但是在随后的开发过程中,你突然需要一个 UITableViewControllerUICollectionViewController...靠!不能使用 CommonViewController 了,因为他是 UIViewController 而不是 UITableViewController

我们该怎么做?新建一个 CommonTableViewController 实现和 CommonViewController 一样的功能,但只是继承改为 UITableViewController?这会产生好多重复代码,绝对是个糟糕透顶的设计。

Composition 来拯救我们啦

当然,政治正确的答案就是:

使用 Composition,不要使用继承啦!

这就意味着为了替代继承,我们需要创建自己的 UIViewController,该 VC 由这些内部类的集合组成,而这些内部类负责提供相应的行为。

在我们的例子中,可以想象一个 BurgerMenuManager 类会提供所有必须的方法来设置汉堡菜单的图标,然后使用 BurgerMenuManager 进行交互,而我们大量的 UIViewControllers 都将会设置一个 property 来引用这个 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,好麻烦~

多重继承

另一个现实原因是:大部分的面向对象语言都不允许多重继承(这是因为存在一个菱形类继承问题

意味着一个类不能有多个父类

假如你实现了一个模型类,用来表示科幻人物。假如你已经创建了 DocEmmettBrown, DoctorWho & TimeLord, IronMan, Superman… 然后他们如何直接关联?一些人能够时间旅行,一些能够太空旅行,还有些所有的事都能做,有些人能飞有些不能,一些是人类一些不是...

class IronMan(钢铁侠)和 class Superman(超人)都能飞,我们可以创建一个会飞的父类 class Flyer,由他来提供飞行方法的实现 func fly() 。但 IronManDocEmmettBrown 都是人类,所以我们还可以创建一个人类的父类 Human,与此同时 SupermanTimeLord 都是外星人 class Alien 的子类。稍等一下... IronMan(钢铁侠)同时继承了 FlyerHuman?这在 Swift 中是不可能的(因为 Swift 也是面向对象编程的语言)

我们在继承中只能二选一,如果让 IronMan (钢铁侠)继承自 Human(人类),那么飞行 func fly() 这个方法该如何实现?我们不能显式地在 Human(人类)中实现飞行这个方法,因为不是所有的人都会飞啊,但是Superman(超人)又需要飞行方法,我们不想再重复一遍。

所以,我们可以在这里使用组合,如同让 class SuperMan 超人类包含一个飞行引擎属性 var flyingEngine: Flyer

但是只是用 superman.flyingEngine.fly() 代替 superman.fly() ,看起来并不是那么优雅。

混合 & 特性

以下是 混合 & 特性(Mixins & Traits)施展手脚的地方

  • 通过继承,一般定义你的类是什么,比如所有的 🐶 Dog 都是一个动物 Animal
  • Traits 特性,定义了你的类可以做什么,比如,所有的动物 Animal 都能吃 eat(),但人类也能吃,神秘博士 Doctor Who 虽然既不是人类也不是动物,但也能吃炸鱼条和蛋冻奶。

所以对于特性来说,他们是什么并不重要,而关键在于他们能做什么

继承定义了这个对象是什么,而特性则定义了这个对象能做什么

更棒的消息是:一个类可以部署很多特性,也就是可以同时做很多事情,这是只从单一父类继承而来的子类所不可企及的,因为他们一次只能做一件事情。

那么在 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 {  
  // we don't implement fly() there so we get the default implementation and hear Clark sing
}

class IronMan: Flyer {  
  // be we can also give a specific implementation if needs be
  func fly() {
    thrusters.start()
  }
}  

Protocols 提供默认实现这一特性棒极了,正如你所愿将 Traits 的概念带进了 Swift

同一身份,多种能力

关于特性最赞的一点就是:特性不依赖于应用这些特性的对象。他们(特性)不关心这些类是什么,继承自何方,他们只是在这些类中定义了一些方法。

这就解决了 Doctor Who 既是时间旅行者又是外星人,以及 Dr Emmett Brown 既是时间旅行者又是人类的难题。再如钢铁侠作为一个人类,和超人作为外星人,但他们都能飞。

你是谁并不能决定你的能力

现在,让我们利用 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)
  }
}

现在能够同时通过他们的身份(继承)和能力(特性/协议)来定义我们的超级英雄了:

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

Superman(超人)和 IronMan(钢铁侠)都使用相同的飞行 fly() 实现,即使他们继承自不同的父类(一个是外星人,另一个是人类),并且 Docotors(博士们)都懂得时间旅行:

let tony = IronMan()  
tony.fly() // prints "I believe I can flyyyyy ♬"  
tony.name  // returns "Tony Stark"

let clark = Superman()  
clark.fly() // prints "I believe I can flyyyyy ♬"  
clark.species  // returns "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  

时空探险

现在让我们探索一种新的空间旅行能力/特性:

protocol SpaceTraveler {  
  func travelTo(location: String)
}

提供一个默认实现:

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

我们可以使用 Swift 的 extensions 为现有类添加共性的协议了,接下来为已定义的英雄角色添加这些能力。如果我们不计较钢铁侠在《复仇者联盟 1》『纽约之战』中英勇地抱着核弹飞到外太空的话,那么只有 Doctor(博士)和 Superman(超人)拥有空间旅行的能力:

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

是的,这就是需要添加超能力,现在他们可以使用 travelTo() 飞往任何地方!代码相当整洁,不是吗?

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

多邀请点人加入我们的派对

现在让我们为更多的英雄赋予能力:

// Come along, Pond!
let amy = Human(name: "Amelia Pond", countryOfOrigin: "UK")  
// Damn, isn't she not a Time and Space Traveler too? Which doesn't make her a TimeLord, though

class Astraunaut: Human, SpaceTraveler {}  
let neilArmstrong = Astraunaut(name: "Neil Armstrong", countryOfOrigin: "USA")  
let laika = Astraunaut(name: "Laïka", countryOfOrigin: "Russia")  
// Wait, Laïka is a Dog, right?

class MilleniumFalconPilot: Human, SpaceTraveler {}  
let hanSolo = MilleniumFalconPilot(name: "Han Solo")  
let chewbacca = MilleniumFalconPilot(name: "Chewie")  
// Wait, isn't MilleniumFalconPilot defined as "Human"?!

class Spock: Alien, SpaceTraveler {  
  init() {
    super.init(name: "Spock", species: "Vulcan")
    // Woops not 100% right
  }
}

呼叫休斯顿,我们遇到一个问题。Laika 不是人类也不是 ChewieSpock 是半人类半瓦肯星人,所以这些定义都是错的。

我们理所应当地认为人类 Human 和外星人 Alien 都可以抽象为单独的类,如果我们继承了这些类,就会被看做是强制认同了这种身份类型。可惜在科幻小说中并不是这样,这才是困扰我们的问题所在。

这也是为什么我们需要在 Swift 中使用 Protocols 并提供协议默认实现的原因。它能帮助我们移除由类继承所带来的限制。

如果将 HumanAlien 由类改为协议,会获得到以下优势:

  • 我们可以定义一个 MilleniumFalconPilot (飞行器)类型而不用强迫他是一个人类,接着让 Chewie 来驾驶
  • 我们可以定义 Laïka 是一个宇航员 Astronaut,即使她并不是一个人类
  • 我们可以定义 Spock 既是人类 Human 又是外星人 Alien
  • 我们甚至可以将继承完全从我们的例子中移除,用结构体 structs 代替类 classes 来定义我们的类型。结构体并不支持继承,但可以遵从多个协议。

协议无处不在

至此可以公布我们的解决方案了:就是完全用协议来取代继承,毕竟,我们并不在乎这些超级英雄是什么?只关心他们有哪些超能力罢了。

我打包了一份 Playground 代码,你可以点这里下载。我用两页的篇幅演示了完全用 ProtocolStructs 是如何实现这一切的,别犹豫,打开看一看!

当然,这并不意味着你必须不惜一切代价避免继承(不要都听 Dalek 的,他们毕竟缺乏感情)。继承仍然有其用武之地,比如 UILabelUIView 子类,你依然能感受到其中的逻辑性。但是,这并不妨碍我们去探索一片新天地 Mixins & Protocols(附带默认实现)

总结

你在 Swift 之路走得越远,就越能意识到这其实是一门面向协议编程的语言,Swift 中大范围应用的协议远比 OC 中要强大的多。毕竟,像 EquatableCustomStringConvertible 以及 -able 这种 Swift 标准库中的协议其实也是混合在一起使用的(Mixins)

通过 Swift 的协议和附带的默认实现,你可以实现 Mixins & Traits(混合 & 特性),不仅如此,你还可以实现抽象类的功能,这一切都会让你的编码之路会更加灵活。

采取 Mixins & Traits 方式组织的代码不仅定义了这些类型能做什么,还说明了他们是什么。更重要的,你可以按需有选择地部署能力。这有点像你去超市购物,为类型挑选他们喜欢的能力放进购物车中,而并不去关心这些类型继承自何方。

回到最初的例子中,你可以创建一个 protocol BurgerMenuManager 以及一个默认实现,然后简单地让你的 View Controllers(UIViewController 或 UITableViewController...)遵从这个协议就好啦,该 VC 会自动获取所有定义在 BurgerMenuManager 中的能力,而不用去担心 UIViewController 的父类是什么!

关于 Protocol Extensions 还能说很多,Don't Panic 我会在今后的文章中徐徐道来,协议扩展可以在很多方面增强你的代码。这篇文章够长啦,今后再写啦,别走开马上回来~


-EOF-

如果感觉此文对你有帮助,请随意打赏支持作者 😘

chengway

认清生活真相之后依然热爱它!

Subscribe to Talk is cheap, Show me the world!

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!