2.2 消息和方法
OOD启思录
对象应当被看作机器,机器只为提出恰当请求的人执行公有接口所定义的操作。因为对象独立于使用者,也因为一些实现了面向对象概念的早期语言的语法,术语“发送消息”用于描述执行对象的行为。当消息被发送至对象,它必须判断是否理解该消息。如果理解,那么对象就把消息映射为一个函数调用,并把自身作为隐含的第一个参数传递过去。对解释语言而言,判断是否理解一个消息是在运行时完成的,而编译语言则是在编译时完成的。
对象行为的名称(或者原型)被称作消息(message)。许多面向对象语言都支持重载函数(overloaded function)或者操作符。这一构造的约定是,系统中的两个函数可以有相同的名字,只要它们的参数类型不同(类内重载)或者所属的类不同(类间重载)就可以了。闹钟类可以有两个不同的set_time消息,一个消息用两个整数作为参数,另一个消息用一个字符串作为参数。这是一个类内重载的例子。
void AlarmClock::set_time(int hours, int minutes);
void AlarmClock::set_time(String time);
此外,闹钟和手表可能都有set_time消息,它们可能都以两个整数作为参数。这是一个类间重载的例子。
void AlarmClock::set_time(int hours, int minutes);
void Watch::set_time(int hours, int minutes);
值得一提的是,消息的组成部分包括函数名、参数类型、返回值类型,以及消息所属的类。这是类的使用者所需知道的主要信息。在一些语言和/或系统中,可能还会有其他信息,比如消息抛出的异常的类型,以及其他相关的同步信息(比如,消息是同步的还是异步的)。类的实现者必须知道如何实现消息。消息的实现,也即实现消息的代码,被称作方法(method)。一旦控制进入方法内部,对接收消息的对象的全部数据成员都是通过隐含的第一个参数引用的。这个隐含的第一个参数在很多语言中都称作“self对象”(C++则偏爱称其为“this对象”)。对象所能响应的消息列表被称作对象的协议(protocol)。
类/对象可以响应两种特殊的消息。第一种是用于为了创建类的对象而调用的操作。这称为类的构造函数(constructor)。类可以有多个构造函数,每个构造函数接受一组不同的初始化参数。例如,我们可以通过传递5个整数参数分别指明小时、分钟、闹铃小时、闹铃分钟、闹铃状态来构造闹钟;我们也可以传递两个字符串和一个整数参数,每个字符串都是“小时:分钟”格式,分别表明时间和闹铃时间;而整数则表明闹铃状态。有的类甚至可以有十几个或者更多构造函数。
类/对象能够响应的第二种特殊的消息是在把对象从系统删除之前清除对象内容的操作。这个操作称为类的析构函数(destructor)。大多数面向对象语言每个类都只有一个析构函数,因为在运行时需要做出的任何决定都可以保存为对象状态的一部分,没有必要再给方法传递额外的参数。我们将在书中多处提及构造函数和析构函数。你可以认为它们是面向对象范型的初始化和清除机制。
经验原则2.2
类的使用者必须依赖类的公有接口,但类不能依赖它的使用者。
这条经验原则背后的基本原理是可复用性。闹钟可以用于卧室(参见图2.4)。使用闹钟的人显然依赖于闹钟的公有界面。但是,闹钟不应当依赖于那个人。如果闹钟依赖于使用者,比如说那个在卧室中用闹钟的人,那么闹钟就无法被用来制造定时锁保险箱,除非把那个人也绑定在保险箱上。这样的依赖性是不受欢迎的,因为我们想要把闹钟用于其他的领域,而不想为此依赖于使用者。所以,最好把闹钟看作一个小型机器,这个小型机器对它的使用者一无所知,它仅仅是执行定义于公有界面的行为,而不管发送消息的是谁。
经验原则2.3
尽量减少类的协议中的消息。
就在几年前,还有人撰文提倡刚好与这条经验原则相反的实践。当时是这样说的:关于这个类的操作,凡是类的实现者能想象到的,将来就会有用户用到。那么,既然如此,为什么不实现这些操作呢?如果你采纳这样的经验原则,那么你肯定会钟爱我的链表类——它的公有接口有4 000个操作。问题时,当你想对两个链表对象执行合并操作时,你认为链表类一定提供了这个操作,所以你依照字母顺序检查消息列表,但是却找不到哪个操作是以merge、union、combine或者你知道的其他同义词命名的。不幸的是,真正的操作是一个重载的加号(在C++中是operator+)。庞大的公有接口的问题是,你永远都无法找到你想要找的东西。这严重损害了接口的可复用性。而如果让接口最小化,我们就可以让系统易于理解,并使组件易于复用。
经验原则2.4
实现所有类都理解的最基本公有接口[例如,拷贝操作(深拷贝与浅拷贝)、相等性判断、正确输出内容、从ASCII描述解析等等]。
如果一个开发者设计和实现的类要被另一个开发者在其他应用程序中复用,那么提供一个常用的最小公有接口常常很有用。1这个最小公有接口包含的功能是人们合理地预期每个类都会有的。我们可以把这个接口当作了解可复用软件代码中类的行为的基础。我们将在第9章中更详细地探讨关于这个最小公有接口的事项。
经验原则2.5
不要把实现细节(例如放置共用代码的私有函数)放到类的公有接口中。
这条经验原则用于为使用者降低类接口的复杂性。基本想法是,类的使用者不想在公有接口中看见他们不用的成员。这些成员属于类的私有区域。如果类的两个方法有一段公共代码,那么就可以创建一个防止这些公共代码的私有函数。把这些公共代码封装成一个独立方法常常会带来方便,但是这个方法并不是一个新的操作,它只是类中两个操作的实现细节。因为是实现细节,所以它应当放在类的私有区域中,而不是公共区域中(参见图2.5)。
为了让你对公共代码私有函数有更贴近实际的了解,你可以认为类X是一个链表,f1和f2是函数insert和remove,公共代码私有函数f是在链表中找到插入点或者删除点位置的操作。
经验原则2.6
不要以用户无法使用或不感兴趣的东西扰乱类的公有接口。
这条经验原则与前一条是相关的,因为类的用户不会想调用公共代码函数,所以把这些函数放在公有接口中只会扰乱类的公有接口。它们并不是类的新操作。有些语言,比如C++,允许在公有接口中错误地包含其他类型的函数。例如,在C++中把抽象类的构造函数放在那个类的公有接口中是合法的,虽然当类的使用者试图使用这样的构造函数时编译器会报告一条语法错误。若遵循更一般化的经验原则2.6,那么这些问题就不会发生了。
1译注:特别是Framework设计尤其如此。很多Framework设计时都在根类中提供了这一最小公有接口(单根继承结构)。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。