Why Are There So Many Rules for Inheritance?
为什么有这么多关于继承的规则?
这一节给出了许多规则,它们能帮你远离与继承相关的麻烦。所有这些规则背后的潜台词都是在说,继承往往会让你和程序员的首要技术使命(即管理复杂度)背道而驰。从控制复杂度的角度说,你应该对继承持有非常歧视的态度。下面来总结一下何时可以使用继承,何时又该使用包含:
■ 如果多个类共享数据而非行为,应该创建这些类可以包含的共用对象。
■ 如果多个类共享行为而非数据,应该让它们从共同的基类继承而来,并在基类里定义共用的子程序。
■ 如果多个类既共享数据也共享行为,应该让它们从一个共同的基类继承而来,并在基类里定义共用的数据和子程序。
■ 当你想由基类控制接口时,使用继承;当你想自己控制接口时,使用包含。
下面就有效地实现成员函数和数据成员给出一些指导建议:
让类中子程序的数量尽可能少 一份针对C++程序的研究发现,类里面的子程序的数量越多,则出错率也就越高(Basili,Briand,and Melo 1996)。然而,也发现其他一些竞争因素产生的影响更显著,包括过深的继承体系、在一个类中调用了大量的子程序、以及类之间的强耦合等等。请在保持子程序数量最少和其他这些因素之间评估利弊。
禁止隐式地产生你不需要的成员函数和运算符 有时你会发现应该禁止某些成员函数——比如说你想禁止赋值,或不想让某个对象被构造。你可能会觉得,既然编译器是自动生成这些运算符的,你也就只能对它们放行。但是在这种情况下,你完全可以通过把构造函数、赋值运算符或其他成员函数或运算符定义为private,从而禁止调用方代码访问它们(把构造函数定义为private也是定义单件类(singleton class)时所用的标准技术,本章后面还会讲到)。
减少类所调用的不同子程序的数量 一份研究发现,类里面的错误数量与类所调用的子程序的总数是统计相关的(Basili,Briand,and Melo 1996)。同一研究还发现,类所用到的其他类的数量越高,其出错率也往往会越高。这些概念有时也称为“扇出/fan out”。
对其他类的子程序的间接调用要尽可能少 直接的关联就已经够危险了。而间接的关联——如account.ContactPerson().DaytimeContactInfo().PhoneNumber()——往往更加危险。研究人员就此总结出了一条“Demeter法则”(Lieberherr and Holland 1989),基本上就是说A对象可以任意调用它自己的所有子程序。如果A对象创建了一个B对象,它也可以调用B对象的任何(公用)子程序,但是它应该避免再调用由B对象所提供的对象中的子程序。在前面account这个例子中,就是说account.ContactPerson()这一调用是合适的,但account.ContactPer-
son().DaytimeContactInfo()则不合适。
这只是一种简化的解释。更多详细信息请参阅本章后面的“更多资源”一节。
一般来说,应尽量减小类和类之间相互合作的范围 尽量让下面这几个数字最小:
■ 所实例化的对象的种类
■ 在被实例化对象上直接调用的不同子程序的数量
■ 调用由其他对象返回的对象的子程序的数量
Constructors
构造函数
接下来给出一些只适用于构造函数(constructor)的指导建议。针对构造函数的这些建议对于不同的语言(C++、Java和Visual Basic)都差不多。但对于析构函数(destructor)而言则略有不同,因此请查阅本章“更多资源”中列出的关于析构函数的资料。
如果可能,应该在所有的构造函数中初始化所有的数据成员 在所有的构造函数中初始化所有的数据成员是一个不难做到的防御式编程实践。
用私用(private)构造函数来强制实现单件属性(singleton property) 如果你想定义一个类,并需要强制规定它只能有唯一一个对象实例的话,可以把该类所有的构造函数都隐藏起来,然后对外提供一个static的GetInstance()子程序来访问该类的唯一实例。它的工作方式如下例所示:
Java示例:用私用构造函数来实现Singleton(单件)
public class MaxId {
// constructors and destructors
private MaxId() {
...
}
...
// public routines
public static MaxId GetInstance() {
return m_instance;
}
...
// private members
private static final MaxId m_instance = new MaxId();
...
}
仅在初始化static对象m_instance时才会调用私用构造函数。用这种方法后,当你需要引用MaxId单件时就只需要简单地引用MaxId.GetInstance()即可。
优先采用深层复本(deep copies),除非论证可行,才采用浅层复本(shallow copies) 在设计复杂对象的时候,你需要做出一项主要决策,即应为对象实现深拷贝(得到深层复本)还是浅拷贝(得到浅层复本)。对象的深层复本是对象成员数据逐项复制(member-wise copy)的结果;而其浅层复本则往往只是指向或引用同一个实际对象,当然,“深”和“浅”的具体含义可以有些出入。
实现浅层复本的动机一般是为了改善性能。尽管把大型的对象复制出多份复本从美学上看十分令人不快,但这样做很少会导致显著的性能损失。某几个对象可能会引起性能问题,但众所周知,程序员们很不擅长推测真正招致问题的代码(详见第25章“代码调优策略”)。
为了不确定的性能提高而增加复杂度是不妥的,因此,在面临选择实现深拷贝还是浅拷贝时,一种合理的方式便是优先实现深拷贝——除非能够论证浅拷贝更好。
深层复本在开发和维护方面都要比浅层复本简单。实现浅拷贝除了要用到两种方法都需要的代码之外,还要增加很多代码用于引用计数、确保安全地复制对象、安全地比较对象以及安全地删除对象等等。而这些代码是很容易出错的,除非你有充分的理由,否则就应该避免它们。
如果你发现确实需要实现浅拷贝的话,Scott Meyers写的《More Effective C++》(1996)一书的第29号调款就C++中的这个问题进行了精辟地阐述。Martin Fowler在《Refactoring》(《重构》, 1999)一书中也论述了在深拷贝和浅拷贝之间相互转换的具体步骤(Fowler把这两种复本对象分别称为引用对象(reference object)和值对象(value object))。







