Python面向对象编程(第4版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.4 隐藏细节并创建公共接口

在面向对象设计中给对象建模的关键目的在于,决定该对象的公共接口是什么。接口是对象允许其他对象访问的属性和方法的集合,其他对象可以通过接口与这个对象进行交互,而不需要(在某些编程语言中也不允许)访问对象的内部工作。

一个真实世界中的示例就是电视机。对我们来说,电视机的接口就是遥控器。遥控器上的每个按钮都代表可以调用的电视机对象的方法。当我们作为调用对象访问这些方法时,我们不需要关心电视机到底是从天线、电缆还是卫星那里获取信号的,也不需要关心传递什么样的电子信号来调节音量,或者声音到底是发往音箱还是耳机的。如果我们打开电视机查看内部构造,例如将音箱和耳机的输出线拆开,那么我们只会失去保修资格。

这个隐藏对象实现细节的过程,被称为信息隐藏,有时候也被称为封装(Encapsulation),但是封装是一个更加宽泛的术语,被封装的数据并不一定是隐藏的。从字面上看,封装就是把属性用胶囊或者封装纸包起来。电视机的外壳封装了电视机的内部状态和行为。我们可以访问它外部的显示器、扬声器和遥控器。我们不能直接访问外壳内部的信号接收器或放大器的排线。

如果我们自己组装一套娱乐系统,那么我们要改变组件的封装程度,组件需要暴露更多的接口,方便我们自己组装。如果我们是物联网设备的制造商,那么我们可能会进一步分解组件,打开外壳,拆开厂家封装起来的内部元器件。

封装和信息隐藏的区别通常是无关紧要的,尤其是在设计层面。很多参考文献会把它们当作同义词。作为Python程序员,我们往往没有也不需要真正的信息隐藏(我们将在第2章中讨论其原因),因此使用含义更广泛的封装也是合适的。

然而,公共接口还是非常重要的,需要仔细设计,因为在未来很多其他类依赖于它的时候就会很难修改。更改接口可能会导致任何调用它的客户端对象(指调用当前对象的其他对象)出错。我们可以随意改变内部构造,例如,让它变得更高效,或者除了从本地还可以从网络上获取数据,而客户端对象仍然可以不加修改地使用公共接口与我们的对象正常交流。另外,如果我们改变了接口中的公共属性名,或者更改了方法参数的顺序或类型,那么对所有的客户端类都需要进行更改。在设计公共接口的时候,应尽量保持简单,永远优先考虑易用性而非编码的难度(这一建议同样适用于用户接口)。因此,有时会看到某些Python的变量名以下画线_开头(比如_name)作为警示,表示它们不是公共接口的一部分。

记住,程序中的对象虽然可能代表真实的物体,但这并不意味着它们是真实的物体,它们只是模型。建模带来的最大好处之一是,可以忽略无关的细节。我小时候做的汽车模型看着很像1956年的雷鸟(一种汽车),但它显然不能跑。这些细节对于年幼还不会开车的我来说太过复杂,也是无关紧要的。模型是对真实概念的一种抽象Abstraction)。

抽象是另一个与封装和信息隐藏相关的面向对象的术语。抽象意味着只处理与给定任务相关的最必要的一层细节,是从内部细节中提取公共接口的过程。汽车司机(Driver)需要与方向盘、油门和刹车装置交互,而不需要考虑发动机、传动系统及刹车系统的工作原理。而如果是机械师(Mechanic),则需要处理完全不同层面的抽象,可能需要优化引擎和调节刹车系统等。以下是汽车两个抽象层面的类图,如图1.6所示。

图1.6 汽车抽象层面的类图

现在,我们又学习了几个概念上有点儿类似的新术语。我们用一句话来总结这些术语:抽象是用独立的公共接口封装信息的过程。私有属性或者方法应该对外隐藏,也就是信息隐藏。在UML图中,我们可以用减号-开头表示一个属性或方法不是公共接口。如图1.6所示,公共接口用加号+开头。

所有这些概念都告诉我们一个重要的设计目标,让我们的模型易于被其他对象理解。这意味着注意细节。

尽量确保方法和属性的名称可以“望文生义”(虽然这很难)。在系统分析过程中,对象通常代表原始问题中的名词,而方法通常是动词,属性可能是形容词或名词。按照这个规律给类、属性和方法命名。

在设计接口时,想象你就是对象,你想要定义清晰的对外责任,但你对如何履行这些责任要保持强烈的隐私偏好。不要让其他对象访问你的内部数据,除非你觉得这对于履行你的责任是有必要的。不要给它们任何可以调用你的执行任务的接口,除非确定这是你的责任的一部分。