购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.6 继承

我们讨论了对象之间的3种关系:关联、组合与聚合。然而,我们还没完全设计好象棋游戏,并且这几种关系似乎仍不够用。我们讨论的玩家可能是人类,也可能是一段人工智能代码。如果我们说“玩家(Player)和人类是关联关系”,或者说“人工智能实现是玩家对象的组成部分之一”,好像都不大对。我们真正需要描述的是“深蓝机器人是一个玩家”,或者“Gary Kasparov是一个玩家”。

“是一个”这种关系是由 继承 Inheritance )产生的。继承是面向对象编程中最有名、最广为人知,也最被过度使用的一种关系。继承有点儿像族谱树。本书作者之一是Dusty Phillips,他的爷爷姓Phillips,而他爸爸继承了这一姓氏,他又从他爸爸那里继承了这一姓氏。与人类继承特征和姓氏不同,在面向对象编程中,一个类可以从另一个类那里继承属性和方法。

例如,在一副国际象棋中有32枚棋子,但只有6种不同的类型(卒、车、象、马、国王和王后),每种类型的棋子在移动时的行为各不相同。所有这些棋子的类有许多共同的属性,如颜色、所属象棋等,但它们同时拥有唯一的形状,以及不同的移动规则。我们来看一下,这6种类型的棋子是如何继承自 Piece 类的,如图1.10所示。

图1.10 棋子如何继承自Piece类

空心箭头形状代表每种棋子类都继承自 Piece 类。所有的子类都自动从父类中继承了 chess_set color 属性。每种棋子都有不同的 shape 属性(当渲染棋盘时被绘制在屏幕上)以及不同的 move 方法,用于移动到新的位置上。

我们知道所有的 Piece 类的子类都需要有一个 move 方法,否则当棋盘需要移动一枚棋子时不知道该怎么办。假如我们想要创建一个新版的象棋游戏,那么可以在里面加入一种新的棋子(巫师,Wizard)。如果这个新类没有 move 方法,棋盘在要移动这种棋子时就会被卡住,而我们现在的设计无法阻止这种事情的发生。

我们可以通过给 Piece 类创建一个假的move方法来解决这个问题。这个方法可能只会抛出一个错误提示信息: 这枚棋子无法被移动 ,而子类用具体的实现来 重写 Override )这个方法。

在子类中重写方法能够让我们开发出非常强大的面向对象系统。例如,我们要实现一个具有人工智能的 Player 类,假设名为 DeepBluePlayer ,我们可以在父类的 Player 类中提供一个 calculate_move 方法,决定移动哪一枚棋子和移动到什么位置上。父类可能只是随机选择一枚棋子和方向进行移动,我们可以在 DeepBluePlayer 子类中用更智能的逻辑重写这个方法。前者可能只适合与新手对抗,而后者可以挑战大师级的选手。重要的是,这个类的其他方法(比如通知棋盘选中了哪枚棋子等)完全不用改动,它们的实现可以在两个类中共享。

在Piece的示例中,为move方法提供一个默认的实现并没有什么意义。我们要做的是要求在子类中必须有move方法。要做到这一点,可以将 Piece 创建为 抽象类 Abstract Class ),并将 move 方法声明为 抽象方法 Abstract Method )。抽象方法基本上就是说:

“在当前类中不提供方法的具体实现,但我们要求所有非抽象的子类必须实现这一方法。”

实际上,我们可以创建一个不实现任何方法的抽象类。这个类只告诉我们它应该做什么,但是完全不告诉我们要如何去做。在某些编程语言中,这种完全抽象的类也被叫作 接口 Interface )。在Python中可以定义只包含抽象方法的类,但这极少见。

1.6.1 继承提供抽象

让我们探索一下面向对象术语中最长的一个单词 多态 Polymorphism ),它是调用同一个方法时根据子类的不同实现而有不同表现的能力。我们已经在前面描述的棋子系统(图1.10)中见过了。如果我们的设计继续深入,可能会发现 Board 对象可以从玩家那里接收移动指令并调用棋子的 move 方法。 Board 不需要知道棋子的类型,只需调用其 move 方法,相应的棋子子类将会知道如何移动,比如使用 Knight )的移动方法或者兵( Pawn )的移动方法。

多态是一个非常酷的概念,但在Python编程中是一个很少出现的单词。Python使用另一种方法让子类看起来像父类。用Python实现的棋盘对象可以接收任何拥有 move 方法的对象,不管它是棋子象、汽车还是鸭子。当 move 方法被调用时,棋子 Bishop )会在棋盘上移动,汽车会驶向某处,鸭子则会看心情游走或飞走。

Python中的这种多态通常被称为 鸭子类型 :如果它走路像鸭子,游泳像鸭子,那么它就是鸭子。我们不关心它是否真的是一只鸭子(继承自鸭子类),只要它可以像鸭子一样会游泳或走路即可。鹅和天鹅就很容易提供这种像鸭子的行为。以鸟类设计为例,鸭子类型允许将来的设计者方便地创建新的鸟类,而不用为所有可能种类的水鸟指定正式的继承层级。在上面的棋子示例中,我们用正规的继承关系涵盖了所有可能的棋子类型。鸭子类型也允许程序员扩展原有的设计,加入一些原来的设计者完全没有考虑的行为。比如,将来的设计者可以创建一个会游泳、会走路的企鹅,它可以使用同样的鸭子接口但不需要继承自鸭子父类。

1.6.2 多重继承

当我们把继承理解为族谱树时,会发现我们可能不单从父母之一那里继承了特征(我们通常同时继承了父亲和母亲的特征)。当陌生人对一位自豪的妈妈说她的儿子眼睛很像爸爸时,妈妈的回答可能是“对,但他的鼻子像我”。

面向对象设计同样可以实现这样的 多重继承 ,允许子类从多个父类那里继承特征。在实践中,多重继承可能是一件棘手的事情,有些编程语言(尤其是Java)甚至严格禁止这样做。然而,多重继承也有它的用处。最常见的是,用于创建包含两组完全不同行为的对象。例如,设计一个对象用于连接扫描仪并将扫描的文件通过传真发送出去,这一对象可能继承自两个完全独立的scanner和faxer对象。

只要两个父类拥有完全不同的接口,子类同时继承这两个类就并没有什么坏处。但是如果两个类的接口有重叠,多重继承就可能造成混乱。扫描仪和传真并没有相同的接口,同时继承它们的功能并没有什么问题。举个相反的示例,有一个摩托车类拥有move方法,还有一个船类也拥有move方法。

我们先想要将它们合并为一个终极水陆两用车,当调用move方法时,生成的类如何知道要执行的操作呢?这需要在设计时详细解释(本书作者之一是一名住在船上的水手,他确实需要解决这个问题)。

Python有一个 方法解析顺序 Method Resolution Order MRO ),可以帮我们确定优先调用哪个方法。使用MRO规则是简单的,但最好的办法是避免多重继承。虽然多重继承作为一种可以把不相关的特征整合在一起的 混入 mixin 技术有一定的帮助,但在很多情况下使用对象 组合 是更简单的选择。

继承是一个非常有力的扩展行为和功能的工具,也是与面向对象设计相比更早的编程方法最具进步性的地方。因此,它通常是面向对象程序员最早学会的工具。但是要注意不要手里拿着锤子就把螺丝钉也看作普通钉子。继承对严格的“是一个”关系是最优的解决方案,但是可能被滥用。程序员经常用继承来共享代码,即使两种对象之间可能只有很少的关联,而不是严格的“是一个”关系。虽然这不一定是坏的设计,但却是一个极好的机会去考虑为何要采用这样的设计,用别的关系或者设计模式是否会更合适。 yKMtp0E+R+BXnXK4eydKWA1Wn8IbYLBHfisDSSWiQnJLaoXq+yXR5AOmYS7xxG8o

点击中间区域
呼出菜单
上一章
目录
下一章
×