转载

超越继承之路:协议混合

只要你学习过面向对象的语言比如 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()   } } 

但是在随后的开发过程中,你突然需要一个 UITableViewController 或 UICollectionViewController ...靠!不能使用 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() 。但 IronMan 和 DocEmmettBrown 都是人类,所以我们还可以创建一个人类的父类 Human ,与此同时 Superman 和 TimeLord 都是外星人 class Alien 的子类。稍等一下... IronMan (钢铁侠)同时继承了 Flyer 和 Human ?这在 Swift 中是不可能的(因为 Swift 也是面向对象编程的语言)

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

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

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

混合 & 特性

超越继承之路:协议混合

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

  • 通过继承,一般定义你的类是什么,比如所有的 :dog: 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 不是人类也不是 Chewie , Spock 是半人类半瓦肯星人,所以这些定义都是错的。

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

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

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

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

协议无处不在

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

超越继承之路:协议混合

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

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

总结

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

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

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

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

超越继承之路:协议混合

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

-EOF-
原文  http://chengway.in/chao-yue-ji-cheng-zhi-lu-xie-yi-hun-he/
正文到此结束
Loading...