要么设计继承并提供文档说明,要么禁用继承

条目 18 中提醒你注意继承没有设计和文档说明的「外来」类的子类化的危险。 那么对于专门为了
继承而设计并且具有良好文档说明的类而言,这又意味着什么呢?
首先,这个类必须准确地描述重写每个方法带来的影响。 换句话说,该类必须文档说明可重写方法
的自用性(self-use)。 对于每个 public 或者 protected 的方法,文档必须指明方法调用哪些可重写方
法,以何种顺序调用的,以及每次调用的结果又是如何影响后续处理。 (重写方法,这里是指非
final 修饰的方法,无论是公开还是保护的。)更一般地说,一个类必须文档说明任何可能调用可重
写方法的情况。 例如,后台线程或者静态初始化代码块可能会调用这样的方法

好的 API 文档应该描述一个给定的方法做了什么工作,而不是描述它是如
何做到的。那么,上面这种做法是否违背了这句格言呢?是的,它确实违背了!这正是继承破坏了封装
性所带来的不幸后果。所以,为了设计一个类的文档,以便它能够被安全地子类化,你必须描述清楚那
些有可能未定义的实现细节。


为了继承而进行的设计不仅仅涉及自用模式的文档设计。为了使程序员能够编写出更加有效的子
类,而无须承受不必要的痛苦,类必须以精心挑选的 protected 方法的形式,提供适当的钩子
hook),以便进入其内部工作中


测试为继承而设计的类的唯一方法是编写子类
如果你忽略了一个关键的受保护的成员,试图编
写一个子类将会使得遗漏痛苦地变得明显。 相反,如果编写的几个子类,而且没有一个使用受保护的成
员,那么应该将其设为私有。 经验表明,三个子类通常足以测试一个可继承的类。 这些子类应该由父
类作者以外的人编写

当你为继承设计一个可能被广泛使用的类的时候,要意识到你永远承诺你文档说明的自用模式以及
隐含在其保护的方法和属性中的实现决定。 这些承诺可能会使后续版本中改善类的性能或功能变得困难
或不可能。 因此, 在发布它之前,你必须通过编写子类来测试你的类。

还有一些类必须遵守允许继承的限制。 构造方法绝不能直接或间接调用可重写的方法。 如果违反
这个规则,将导致程序失败。 父类构造方法在子类构造方法之前运行,所以在子类构造方法运行之前,
子类中的重写方法被调用。 如果重写方法依赖于子类构造方法执行的任何初始化,则此方法将不会按预
期运行。

如果你决定在为继承而设计的类中实现 Cloneable Serializable 接口,那么应该知道,
由于 clone readObject 方法与构造方法相似,所以也有类似的限制: clone
readObject 都不会直接或间接调用可重写的方法。 readObject 的情况下,重写方法将在子类
的状态被反序列化之前运行。 在 clone 的情况下,重写方法将在子类的 clone 方法有机会修复克
隆的状态之前运行。 在任何一种情况下,都可能会出现程序故障。 在 clone 的情况下,故障可能会
损坏原始对象以及被克隆对象本身。 例如,如果重写方法假定它正在修改对象的深层结构的拷贝,但是
尚未创建拷贝,则可能发生这种情况。
最后,如果你决定在为继承设计的类中实现 Serializable 接口,并且该类有一个
readResolve writeReplace 方法,则必须使 readResolve writeReplace 方法设置为
受保护而不是私有。 如果这些方法是私有的,它们将被子类无声地忽略。 这是另一种情况,把实现细
节成为类的 API 的一部分,以允许继承。
到目前为止,设计一个继承类需要很大的努力,并且对这个类有很大的限制。 这不是一个轻率的
决定。 有些情况显然是正确的,比如抽象类,包括接口的骨架实现(skeletal implementations)(详见
20 条)。 还有其他的情况显然是错误的,比如不可变的类(详见第 17 条)。
但是普通的具体类呢? 传统上,它们既不是 final 的,也不是为了子类化而设计和文档说明
的,但是这种情况是危险的。每次修改这样的类,则继承此类的子类将被破坏。 这不仅仅是一个理论问
题。 在修改非 final 的具体类的内部之后,接收与子类相关的错误报告并不少见,这些类没有为继
承而设计和文档说明。
解决这个问题的最佳方法是禁止对在设计上和文档说明中都不支持安全子类化的类进行子类化。
这有两种方法禁止子类化。 两者中较容易的是声明类为 final 。 另一种方法是使所有的构造方法都
是私有的或包级私有的,并且添加公共静态工厂来代替构造方法。 这个方案在内部提供了使用子类的灵
活性,在条目 17 中讨论过。两种方法都是可以接受的。
这个建议可能有些争议,因为许多程序员已经习惯于继承普通的具体类来增加功能,例如通知和同
步等功能,或限制原有类的功能。 如果一个类实现了捕获其本质的一些接口,比如 Set List
Map ,那么不应该为了禁止子类化而感到愧疚。 在条目 18 中描述的包装类模式为增强功能提供了继
承的优越选择。
如果一个具体的类没有实现一个标准的接口,那么你禁止继承可能给一些程序员带来不便。 如果你
觉得你必须允许从这样的类继承,一个合理的方法是确保类从不调用任何可重写的方法,并文档说明这
个事实。 换句话说,完全消除类的自用(self-use)的可重写的方法。 这样做,你将创建一个合理安全
的子类。 重写一个方法不会影响任何其他方法的行为。
你可以机械地消除类的自我使用的重写方法,而不会改变其行为。 将每个可重写的方法的主体移动
到一个私有的帮助器方法,并让每个可重写的方法调用其私有的帮助器方法。 然后用直接调用可重写
方法的专用帮助器方法来替换每个自用的可重写方法。
简而言之,专门为了继承而设计类是一件很辛苦的工作。你必须建立文档说明其所有的自用模式,
并且一旦建立了文档,在这个类的整个生命周期中都必须遵守。如果没有做到,子类就会依赖父类的实
现细节,如果父类的实现发生了变化,它就有可能遭到破坏。为了允许其他人能编写出高效的子类,你
还必须暴露一个或者多个受保护的方法。除非意识到真的需要子类,否则最好通过将类声明为
final ,或者确保没有可访问的构造器来禁止类被继承

猜你喜欢

转载自www.cnblogs.com/lIllIll/p/12670932.html