且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

《OOD启思录》—第2章2.1节类和对象导引

更新时间:2022-09-17 08:45:52

本节书摘来自异步社区《OOD启思录》一书中的第2章2.1节类和对象导引,作者【美】Arthur J.Riel,更多章节内容可以访问云栖社区“异步社区”公众号查看。

第2章 类和对象:面向对象范型的建材
OOD启思录
2.1 类和对象导引
面向对象范型使用类和对象的概念作为基本建筑材料。应用程序的分析、设计、实现模型一致地使用这些概念。通过现实世界中的例子来解释这些概念是***方案。如果有一屋子的人,你问:“给你们所需的全部零件,谁能装配出一只闹钟”?最多有一两个人会举手。但如果你问他们“这个房间里谁能够把闹铃设到早上9点”,那么我可以放心地和你打赌,大多数人都会举手。大多数人会使用闹钟,但不会装配闹钟,这难道不荒谬吗?对这个问题,你最直接的反应当然是“当然不荒谬,你的问题才荒谬呢”!

在现实世界中,有很多东西是我们会使用但不会制造的,比如冰箱、汽车、复印机、计算机等等。这只是列举了一小部分。为什么我们可以不知道它们的实现却能轻松使用它们?因为它们被设计为通过一个精确定义的公有界面[1]来使用。这个公有界面极大地依赖于内部的实现,但又向用户隐藏了内部实现。这一设计策略还允许闹钟制造商把目前闹钟用到的60个小零件替换成进口的3个子部件,而闹钟的使用者对此不会有意见。

公有界面与实现的另一个例子可见于汽车行业。很少有驾车者介意机械点火系统(配电盘、电插座、电容器)到电子点火系统的转变。为什么?因为公有界面保持了一致,改变的只是实现。但是,请想象一下,如果你去汽车经销 商处购买新车,经销商递给你一把钥匙,并让你试车。你坐在驾驶座上,寻找点火装置的钥匙孔。你从驾驶杆找到仪表板,再搜索相邻区域,却寻不着钥匙孔。你问经销商如何发动汽车。经销商说,“噢,这个型号是这样的,你用钥匙打开旅行箱,然后你会看到一个红色按钮。按一下那个按钮汽车就会启动了。”你会感到不安,因为汽车制造商改变了你熟悉的公有界面。

面向对象范型的一个基本想法就是这样。所有构成系统的实现细节都应该隐藏在精确定义并且一致的公有接口后面。使用这些构造的用户需要知道这个公有接口,但你不让他们看见实现细节。这样,如果需要,实现者就可以改变实现细节,只要公有接口保持不变就行。我经常旅行,可以向你保证,不需要知道实现细节就能使用闹钟实在很方便。我曾在许多旅馆住过,用过很多种闹钟,有用电的,有需要上发条的,有依靠电池的,有数字型的,有模拟型的。但是,我坐在飞机上时从未忧虑过不会使用将到达的旅馆房间里的闹钟。

我提到“闹钟”这个词后大多数读者都会知道我指的是什么,虽然可能你身边并没有闹钟。为什么?因为你曾经看见过很多闹钟,并且知道,所有的闹钟都有一些共同的属性,比如时间、闹铃时间(都按小时和分钟显示)以及闹铃开关。你还知道,你看到过的所有闹钟都允许你设置它们的时间和闹铃时间,并且允许你打开或者关闭闹铃。这样,你就有了一种叫做“闹钟”的概念,这一概念用一个简洁的组合表示了所有闹钟的数据和行为。这种概念称作类(class)。而你拿在手上的闹钟实物叫做闹钟类的对象(object)或者实例(instance)。类和对象之间的关系叫做实例化关系(instantiation relationship)。我们说,闹钟对象是从闹钟类实例化(instantiate)而来,闹钟类是你遇到的所有闹钟对象的泛化(generalization)[2](参见图2.1)。


《OOD启思录》—第2章2.1节类和对象导引https://yqfile.alicdn.com/81e5d23c815509eb0b9e6c62a23b4eb8c0f9b640.png" >

如果我告诉你,我的闹钟从我的床头几上跳起来,咬了我一口,然后去追邻居的猫了,你一定会认为我疯了。但如果我告诉你,我的狗做了这些事情,你会觉得这挺合理的。这是因为,类的名字不仅意味着一组属性,还表示实体的行为。这种数据和行为的双向联系是面向对象范型的基石之一。

一个对象一定会有如下4个重要方面:

1.它自己的身份标识(可能只是它在内存中的地址);

2.它的类的属性(通常是静态的)和这些属性的值(通常是动态的);

3.它的类的行为(从实现者的角度看);

4.它的类的公开接口(从用户的角度看)。

将这一讨论置于软件开发的语境,类可以被实现为一个结构定义以及一组可以处理这个结构的操作。在过程式语言中,任给一个函数,很容易找出数据依赖性。只要检查函数实现并看一下所有参数、返回值以及局部变量声明的数据类型就可以了。但是,如果你想要找出一个数据定义的函数依赖性,那你就不得不检查全部代码,寻找依赖于这个数据的函数。而在面向对象模型中,两种依赖性(函数对数据的依赖性和数据对函数的依赖性)都现成摆明在那里了。对象是类数据类型的变量。它们的内部细节只对同它们的类关联的那组函数可见。这种对内部细节的访问限制称作信息隐藏(information hiding)。在很多面向对象语言中,这种隐藏不是强制的,这样我们就有了第一条(也是最重要的一条)经验原则。

经验原则2.1
所有数据都应该隐藏在它所在的类内部。

违反这条经验原则意味着你不重视可维护性。面向对象范型所带来的益处,大部分归因于在设计阶段和实现阶段始终确保信息隐藏。如果你把数据设定为公有,那么就很难判断系统哪部分的功能依赖于这个数据。事实上,这样一来,数据变动与函数的映射关系就和面向动作范型一模一样了。我们不得不检查所有的函数以判断哪些函数依赖于公有数据。

有时开发者会争辩说,“我需要把这个数据设为公有,因为……”在这种情况下,开发者应该问自己,“我到底要用这个数据来做什么?为什么不是类为我提供这个操作?”在所有这类情况下,问题出在类缺少了一个必需的操作。比如,考虑图2.2中的File类。开发者出人意料地认为,byte_offset数据成员应该是公有的,这样才能允许随机I/O访问。但是,我们实际上需要的是执行随机访问任务的操作。(如果你不是C程序员,那么我在这里补充说明一下:fseek和ftell和标准C库函数,用于执行文件的随机I/O访问。)冒昧地认为“我们可以把这个数据设为公有,因为它永远也不会改变”的程序员请注意,Murphy关于编程的一条定理表明,这是第一个需要改变的数据。


《OOD启思录》—第2章2.1节类和对象导引

通过下面的例子,我们可以进一步描述数据隐藏带来的好处。这是一个点类,它的实现采用了直角坐标系(参见图2.3)。天真的设计者可能会争辩说,我们可以把点的x坐标和y坐标设为公有,因为实现永远也不会改变。但是,不可避免地,某些新的需求会迫使你改用极坐标系,从而会影响使用这个点类的所有用户。如果我们把数据隐藏起来,那么只有类的实现者需要改变他们的代码。


《OOD启思录》—第2章2.1节类和对象导引