替换原则(Liskov Substitution Princple)是OCP的重要支撑,也是继承关系设计的基本原则。其关键在于,不能单方面的、孤立的思考设计,应多从使用者角度考虑问题。设计类结构时,应该父类与子类之间的可替换性,而不是只考虑ISA关系。
在OCP背后是抽象和多态,Java中通过继承来支持抽象和多态。那么有没有设计规则来管理继承呢?最好的继承层次是怎样的?有哪些陷阱会导致我们的继承不符合OCP?这些问题就是LCP需要解决的。
文中Rectangle与Square的例子,从作者角度考虑,Square类中setWidth和setHeight同时设置width和height,这是没问题的。但由于Square继承于Rectangle,就会有使用者开发下面的代码
void g (Rectangle r) {
r.setHeight(4);
r.setWidth(5);
assertTrue(r.getArea() == 20;
}
当传入一个Square对象时,就出错了。我们不能假设每个使用者都清楚Square类中对set方法的修改,毕竟当传入是Rectangle对象时,上述assert是完全合理的。
SUBTYPES MUST BE SUBSTITUTABLE FOR THEIR BASE TYPES.
Barbara Liskov first wrote this principle in 1988. She said,
What is wanted here is something like the following substitution property: if for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
这里强调的是用程序中对象用子类替换后,其行为不会发生变化。
还是看上面Rectangle和Square的例子,Square类中setWidth和setHeight同时设置width和height,行为与Rectangle不一致,因此违反了LSP。
子类和父类行为的原则必须一致,如果出现不一致,说明它们之间就不应该是继承关系。
LSP强调从用户的角度进行设计。The validity of a model can only be expressed in terms of its clients. 但谁知道用户会作哪些假设呢?很多情况都不好预测,预测过多,就会陷入Needless Complexity中。因此,与其他原则类似,除了明显的违反LSP外,我们只有在嗅到Fragility smell 时才考虑使用该原则进行设计。
Like all other principles, it is often best to defer all but the most obvious LSP violations until the related Fragility has been smelled.
那为什么明明Square属于Rectangle的子集,不能进行继承呢?这是因为从行为上看,Square不是Rectangle。面向对象设计时,更注重行为,而不是属性。这也是面向接口编程。
文中Set例子中对LSP的违反,我们在开发中更容易遇到。
Set已经有两个子类:Bounded Set, Unbounded Set, 这时要添加第三个子类:PersistentSet。PersistentSet将行为委托给了Third Party Persistent Set,但这个第三方set中的元素都继承于Persistent Object。这样就有了一个隐形限制:任何添加到PersistentSet中元素都必须继承于Persistent Object。而之前的Bounded Set和Unbounded Set则没有这个限制,之前运行没有代码的问题,当使用Persistent Set时可能会抛出异常。
违反LSP的Set
也许我们可以通过约定去让开发人员注意到这一点,作者在最开始甚至增加了一个模块用于屏蔽PersistentSet,该模块执行实际内容与PersistentObject之间的转换,开发人员无需知道PersistentSet的存在。但随着使用范围的扩大,还是有部分不知道这个模块的开发人员会直接去使用PersistentSet,导致异常的产生。
解决方法:
Factoring instead of Deriving. 当A继承B违反了LSP时,取消继承,提取A和B的共同行为,放到一个新的抽象类中。
Rebecca Wirfs-Brock, Brian Wilkerson, and Lauren Wiener say:
We can state that if a set of classed all support a common responsibility, they should inherit that responsibility from a common supperclass.
一种解决方案
很多时候,我们都会折中一下,容忍一下违反LSP的行为,毕竟没有完美的代码。但一定要慎重考虑违反LSP的情况,因为这样会增加系统的复杂度,一旦你违反LSP,你就要单独考虑每个子类。
最后,当你发现派生类中的退化函数或派生类中抛出异常时,就要想想是否违反了LSP。
出现退化函数就意味着父类中部分行为子类中不具备,不能完全替换父类。
派生类中抛出异常则会导致调用者收到计划外的异常。