建议:要么为继承而设计,并提供文档说明,要么就禁止继承。

首先,该类的文档必须精确地描述覆盖每个方法所带来的影响。换句话说,该类必须有文档说明他可覆盖(override)的方法的自用性(self-use)。对于每个公有的或受保护的方法或者构造器,它的文档必须指明该方法或者构造器调用了哪些可覆盖的方法,是以什么顺序调用的,每个调用的结果又是如何影响后续的处理过程的(所谓可覆盖(override)的方法,是指非final的,公有的或受保护的)。更一般的,类必须在文档中说明,在哪些情况下会调用可覆盖的方法。例如,后台的线程或者静态的初始化器(initializer)可能会调用这样的方法。

按惯例,如果方法调用到了可覆盖的方法,在它的文档注释的末尾应该包含关于这些调用的描述信息。这段描述信息要以这样的句子开头:“This implementation.(该实现......)”。这样的句子不应该被认为这时在表明该行为可能会随着版本的变迁而改变。它以为这这段描述关注该方法的内部工作情况。

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

为了继承而进行的设计不仅仅涉及自用模式的文档设计。为了使程序员能够编写出更加有效的子类,而无需承受不必要的痛苦,类必须通过某种形式提供适当地钩子(hook),以便能够进入到它的内部工作流程中,这种形式可以是精心选择的受保护的(protected)方法,也可以是受保护的域,后者比较少见。

对于为了继承而设计的类,唯一的测试方法就是编写子类。如果遗漏了关键的受保护成员,尝试编写子类就会使遗漏所带来的痛苦变得更加明显。相反,如果编写了多个子类,并且无一使用受保护的成员,或许就应该把它做成私有的。经验表明,3个子类通常就足以测试一个可扩展的类。除了超类的创建者之外,都要编写一个或者多个这种子类。

在为了继承而设计有可能被广泛使用的类时,必须要意识到。对于文档中所说明的自用模式(self-use pattern),以及对于其受保护方法和域中所隐含的实现策略,你实际上已经做出了永久的承诺。这些承诺使得你在后续的版本中提高这个类的性能或者增加新功能都变得非常困难,甚至不可靠。因此,必须在发布之前先编写子类对类进行测试。

还要注意,因继承而需要的特殊文档会打乱正常的文档信息,普通的文档被设计用来让程序员可以创建该类的实例,并调用类中的方法。

为了允许继承,类还必须遵守其他一些约束。构造器绝不能调用可被覆盖的方法,无论直接调用还是间接调用。如果违反了这条规则,很有可能导致程序失败。超类的构造器在子类的构造器之前运行,所以,子类中覆盖版本的方法将会在子类的构造器运行之前就先被调用。如果该覆盖版本的方法依赖于子类构造器所执行的任何初始化工作,该方法将不会如预期般地执行。

如果你决定在一个为了继承而设计的类中实现Cloneable或者Serializable接口,就应该意识到,因为clone和readObject方法在行为上非常类似于构造器,所以类似的限制规则也是适用的:无论是clone还是readObject,都不可以调用可覆盖的方法,不管是以直接还是间接的方式。对于readObject方法,覆盖版本的方法将子类的状态被反序列化(deserialized)之前先被执行;而对于clone方法,覆盖版本的方法则是在子类的clone方法有机会修正被克隆对象的状态之前先被运行。无论哪种情形,都不可避免地导致程序失败。在clone方法的情形中,这种失败可能会同时损害到原始的对象以及被克隆的对象本身。例如,如果覆盖版本的方法假设他正在修改对象深层结构的克隆对象的备份,就会发生这种情况,但是该备份还没有完成。

最后,如果你决定在一个为了继承而设计的类中实现Serializable,并且该类有一个readResolve或者writeReplace方法,就必须使readResolve或者writeReplace成为受保护的方法,而不是私有的方法。如果这些方法是私有的,那么子类将会不声不响地忽略掉这两个方法。这正是“为了允许继承,而把实现细节变成一个类的API的一部分”的另一种情形。

到现在为止,应该很明显:为了继承而设计类,对这个类会有一些实质性的限制。这并不是很轻松就可以承诺的决定。在某些情况下,这样的决定很明显是正确的,比如抽象类,包括接口的骨架实现(skeletal implementation)。但是,在另外一些情况下,这样的决定却很明显是错误的,比如不可变的类。

但是,对于普通的具体类应该怎么办呢?他们既不是final的,也不是为了子类化而设计和编写文档,所以这种状况很危险。每次对这种类进行修改,从这个类扩展得到的客户类就有可能遭到破坏。这不仅仅是个理论问题。对于一个并非为了继承而设计的非final具体类,在修改了它的内部实现之后,接收到与子类化相关的错误报告也并不少见。 这个问题的最佳解决方案是,对于那些并非为了安全的进行子类化而设计和编写文档的类,要禁止子类化。有两种办法可以禁止子类化。比较容易的办法是把这个类声明为final的。另一种办法是把所有构造器都变成私有的,或者包级私有的,并增加一些公有的静态工厂来替代构造器。他为内部使用子类提供了灵活性。这两种办法都是可以接受的。

这条建议可能会引来争议,因为许多程序员已经习惯于对普通的具体类进行子类化,以便增加新的功能设施,比如仪表功能(instrumentation,如计数显示等)、通知机制或者同步功能,或者为了限制原有类中的功能。如果类实现了某个能够反映其本质的接口,比如Set、List或者Map,就不应该为了禁止子类化而感到后悔。

如果具体的类没有实现标准的接口,那么禁止继承可能会给有些程序员带来不便。如果你认为必须允许从这样的类继承,一种合法的办法是确保这个类永远不会调用它的任何可覆盖的方法,并在文档中说明这一点。换句话说,完全消除这个类中可覆盖方法的自用特性。这样做之后,就可以创建“能够安全的进行子类化”的类。覆盖方法将永远也不会影响其他任何方法的行为。

你可以机械的消除类中可覆盖方法的自用特性,而不改变它的行为。将每个可覆盖方法的代码体移到一个私有的“辅助方法(helper method)”中,并且让每个可覆盖的方法调用它的私有辅助方法。然后,用“直接调用可覆盖方法的私有辅助方法”来代替“可覆盖方法的每个自用调用”。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值