替换原则(LSP - Liskov Substitution Principle)

如何设计最佳的继承层次?  怎样避免类层次结构不符合OCP ?

答案就是替换原则(LSP): 子类型必须能够替换调它们的基类型。


代码违反LSP原则的形式:

1、使用if 或 if/else 去确定一个对象的类型,从而选择执行对应的行为。

2、子类功能少于基类的,通常是不能替换的。

3、派生类的方法中添加了基类不会抛出的异常。

4、更微妙的违规: 正方形(Square)和矩形(Rectangle)的例子。

让正方形继承矩形,并重写矩形的setWidth(width=height)、setHeight(width=height)、area(width*width)方法。

看起来没什么问题, 在很多场景可用正确使用。

但如果有个客户端程序方法:

void g(Rectangle& r)

{

   r.setWidth(10);

   r.setHeight(4);

   assert(40==r.area());

}

传入的是Square对象,则会出错,发生断言。


上面这个例子可得出一个重要结论:一个模型,如果孤立的看,并不具有真正意义上的有效性。模型的有效性,只能通过它的客户程序来检验。

如果孤立的看,正方形(Square)和矩形(Rectangle例子确实是有效的,并且从数学意义上看,正方形确实是一种特殊的长方形。

但如果从对基类做出一些合理假设(对于Rectangle类型来说,断言确实是成立的)的程序员角度来看,这个模型就有问题。

在考虑一个特定设计是否恰当时,不能孤立的来看这个解决方案。必须要从使用者的合理假设(常常以断言的形式出现在为基类编写的单元测试中。测试驱动开发的好理由)来审视它。


怎样才知道使用者会做出什么样的合理假设呢?

事实上是很难预测的。如果试图去预测所有这些假设,我们的系统很可能会充满许多不必要的复杂性。

因此,像应用其它原则一样,最好的方法是只预测那些明显违反LSP原则的情况,至于其它的,等它们真正出现的时候再去处理吧。


那么究竟是怎么回事,Square 和Rectangle 这个显然合理的模型为什么会有问题?毕竟Square是一个Rectangle。难道它们之间不存在IS-A

关系吗?

对于那些不是g函数编写者的人来说,Square可以是Rectangle,但对g来说,Square对象绝对不是Rectangle对象。

为什么?因为Square对象的行为和g所期望的Rectangle对象的行为方式并不相容。

从行为方式来看,Square并不是Rectangle, 行为方式才真正是软件所关注的。

OOD中IS-A关系是就行为方式而言的, 而行为方式是可以假设的。


对类设计者而言,对于行为方式可以假设这个概念,或许会感到不安。 我们如何才知道客户真正的设想呢?

有一种技术可以使这种假设明确化, 那就是基于契约设计(Design By Contract, 简称DBC)。

使用DBC,类的设计者可以显式的规定类的使用契约。

即为每个方法声明前置条件、后置条件。

要使方法正确执行,那么输入必须满足前置条件。 那么,可以保证方法执行完后,后置条件为真。

客户可以依赖这个契约,保证正确的使用类。


上例中, Rectangle::setWidth的后置条件可为:

width==w && height==old.height

Square::setWidth的方法实现为:

width=w

height=w

明显对Square来说,height != old.height

对客户来说,是通过基类来编程的,他们只知道基类(Rectangle)的契约, 很明显Square违反了Rectangle的契约,也违反了LSP原则。

因此,我们不应该让Square继承自Rectangle


总结:子类型的正确定义是:“可替换基类的”。 这里的可替换性包括显示的,或隐式的契约。







版权声明:本文为qq_18497495原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。