只要你学习过面向对象的语言比如 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,不要使用继承啦!
这就意味着为了替代继承,我们需要创建自己的 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
都是一个动物 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 我会在今后的文章中徐徐道来,协议扩展可以在很多方面增强你的代码。这篇文章够长啦,今后再写啦,别走开马上回来~