超越继承之路:协议混合
只要你学习过面向对象的语言比如 ObjC ,都知道继承的概念,他的一个用途是在多个类之间共享代码。但是这种解决方案存在一些问题。这篇文章我们来初探一下 Swift 的协议扩展,以及如何混合使用这些协议 - Mixins,英文原文地址
如果感觉太长了,读不下去,可以直接下载代码 Swift Playground Code
继承的问题
比如你有个 app,其中有大量的UIViewController
类都要共享相同的行为,例如他们都有一个相同样式的汉堡菜单。你不想在每个View Controllers
中都实现一遍『汉堡菜单』的逻辑(设置 leftBarButtonItem
,按钮点击时打开/关闭菜单)
解决方法很简单,创建一个通用的CommonViewController
,继承自UIViewController
,然后实现所有的行为,接着让其他的UIViewController
继承自这个CommonViewController
,而不是直接继承自UIViewController
。通过这种方式,这些 VC 将拥有这些相同的方法和行为,不需要再每次都自己实现一遍了。
|
|
但是在随后的开发过程中,你突然需要一个UITableViewController
或 UICollectionViewController
…靠!不能使用CommonViewController
了,因为他是 UIViewController
而不是UITableViewController
!
我们该怎么做?新建一个CommonTableViewController
实现和CommonViewController
一样的功能,但只是继承改为UITableViewController
?这会产生好多重复代码,绝对是个糟糕透顶的设计。
Composition 来拯救我们啦
当然,政治正确的答案就是:
使用 Composition,不要使用继承啦!
这就意味着为了替代继承,我们需要创建自己的UIViewController
,该 VC 由这些内部类的集合组成,而这些内部类负责提供相应的行为。
在我们的例子中,可以想象一个BurgerMenuManager
类会提供所有必须的方法来设置汉堡菜单的图标,然后使用BurgerMenuManager
进行交互,而我们大量的UIViewControllers
都将会设置一个 property
来引用这个BurgerMenuManager
,进而与汉堡菜单交互。
|
|
可悲的是这样也太笨重了吧,每次都需要引用一个中间对象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
都是一个动物Animal
- 而
Traits
特性,定义了你的类可以做什么,比如,所有的动物Animal
都能吃eat()
,但人类也能吃,神秘博士 Doctor Who 虽然既不是人类也不是动物,但也能吃炸鱼条和蛋冻奶。
所以对于特性来说,他们是什么并不重要,而关键在于他们能做什么
继承定义了这个对象是什么,而特性则定义了这个对象能做什么
更棒的消息是:一个类可以部署很多特性,也就是可以同时做很多事情,这是只从单一父类继承而来的子类所不可企及的,因为他们一次只能做一件事情。
那么在 Swift 中该如何应用?
带默认实现的协议
在 Swift 2.0 中,当你定义了一个 protocol
,可以通过 extension
为其附加相关的实现方法:
|
|
鉴于此,我们创建了一个遵守 Flyer
协议的类或结构体对象,该对象会免费获得 fly()
方法!
你可以根据需要随时重载这个默认实现,当然也可以什么都不做,这样就自动获得一个默认实现:
|
|
接着给出默认实现:
|
|
关于定义超级英雄角色这一点上(他们是谁),我们依然先使用继承,下面来实现几个父类:
|
|
现在能够同时通过他们的身份(继承)和能力(特性/协议)来定义我们的超级英雄了:
|
|
Superman
(超人)和 IronMan
(钢铁侠)都使用相同的飞行 fly()
实现,即使他们继承自不同的父类(一个是外星人,另一个是人类),并且 Docotors(博士们)都懂得时间旅行:
|
|
时空探险
现在让我们探索一种新的空间旅行能力/特性:
|
|
提供一个默认实现:
|
|
我们可以使用 Swift 的 extensions
为现有类添加共性的协议了,接下来为已定义的英雄角色添加这些能力。如果我们不计较钢铁侠在《复仇者联盟 1》『纽约之战』中英勇地抱着核弹飞到外太空的话,那么只有 Doctor(博士)和 Superman(超人)拥有空间旅行的能力:
|
|
是的,这就是需要添加超能力,现在他们可以使用 travelTo()
飞往任何地方!代码相当整洁,不是吗?
|
|
多邀请点人加入我们的派对
现在让我们为更多的英雄赋予能力:
|
|
呼叫休斯顿,我们遇到一个问题。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 我会在今后的文章中徐徐道来,协议扩展可以在很多方面增强你的代码。这篇文章够长啦,今后再写啦,别走开马上回来~
原文链接:http://chengway.in/chao-yue-ji-cheng-zhi-lu-xie-yi-hun-he/
PS:现在JAVA的JDK1.8中也支持了接口的默认实现,也就是说,JAVA也可以面向协议来编程了.