子类型必须能够替换掉他们的父类型
这里的所有观点摘抄自《敏捷软件开发原则、模式与实践》,原著Robert C. Martin,邓辉等译。
李氏替换原则
假设有一个函数f,接受一个指向某个基类B的指针或者引用。如果把B的派生类D的对象作为参数传给f,会导致f出现错误行为。那么D就违反了LSP。
另外一种情况,假定要对传入的对象D做测试,看D是否满足f所需要的条件。这个测试就会违反OCP。原因是什么呢?因为f需要的行为是B所具有的行为。
但是对于B的派生类却没有这样的要求。要让测试D的用例通过,就必须修改f,这样就违反了对修改封闭的原则,即OCP。
看一个违反LSP的例子:
struct Point (double x, y);
struct Shape {
enum ShapeType { square, circle } itsType;
Shape(ShapeType t) : itsType(t) {}
}
struct Circle: public Shape
{
Circle(): Shape(circle) {};
void Draw() const;
Point itsCenter;
double itsRadius;
}
struct Square: public Shape
{
Square(): Shape(square) {};
void Draw() const;
Point itsTopLeft;
double itsSide;
}
void DrawShape(const Shape& s)
{
if (s.itsType == Shape::square)
static_cast<const Square&>(s).Draw();
else if (s.itsType == Shape::circle)
static_cast<const Circle&>(s).Draw();
}
很明显的,上述代码违反了OCP原则,因为我们新增一种形状,都会导致源代码的修改,需要添加新的if else。
这个例子也理所当然的违反了LSP,因为派生类Circle和Square并不能替换掉Shape. 除非Shape中包含Draw并且将Draw设置为虚函数。
另外一个有名的例子就是矩形和正方形的问题。我们在数学中会把正方形当做矩形的一个特例。从而影响到我们在开发过程中,也会理所当然的
认为正方形应该是从矩形派生而来。
但实际上应该考虑清楚,正方形和矩形,并非是一个继承的关系。矩形的长宽可以独立调整,如果正方形派生自矩形,那么正方形的长宽也应该可以
独立的调整。但实际上正方形长宽是固定相等的。
有人可能会说,可以在正方形的类中特殊处理。例如:
void Square::SetWidth(double w)
{
Rectangle::SetWidth(w);
Rectangle::SetHeight(w);
}
void Square::SetHeight(double w)
{
Rectangle::SetWidth(w);
Rectangle::SetHeight(w);
}
class Rectangle
{
public:
virtual void SetWidth(double w);
virtual void SetHeight(double w);
}
void f(Rectangle& r)
{
r.SetWidth(23);
}
上面的代码看起来运行没有问题,Square类的对象也可以作为参数传入f,但实际上隐藏了本质上的问题。也就是认知上的问题。
考虑如下的代码:
void ch(Rectange& r)
{
r.SetWidth(22);
r.SetHeight(30);
}
如果传入了一个正方形的对象,我们最终期望的正方形边长应该是多大?使用者只会知道,可以传入正方形,并期望传入之后
满足矩形的行为。但实际上会出现令人迷惑的结果。
这个有点类似于挂羊头卖狗肉的感觉,实际上需要的羊肉的味道,但是你给的确实狗肉,出来的结果可想而知。
如何识别
经过上面的几个例子,可以看到,如果单单从一个问题来看,也许系统是满足条件的。违反不违反LSP并没有多大的坏处。但是,
从设计的使用者的角度,就可以看出,违反了LSP原则会给系统的稳定性带来多大的影响。
但是有谁能够知道设计的使用者会做出怎样的合理假设呢?大多数这样的假设都很难预测。事实上如果试图去猜测使用者的所有
假设,会让系统无比复杂。最好的方法就是只预测那些明显对于LSP违反的情况。
继续深入的思考一下,LSP所描述的是一个严格的IS-A的关系。我们从认知上来看,正方形似乎是一个矩形,是它的一个特例。
但是从使用者的角度,正方形却处处和长方形不兼容。那么真正可以区分是否满足LSP(也就是满足IS-A)的条件是什么呢?
- 对象的行为方式
因为正方形和长方形所预期的行为方式不相容,所以不满足LSP。
所以,考察一个继承关系是否满足LSP,最终看的是派生的类和基类是否具有相同的行为方式。
具体的考察行为方式的办法有两种:
- 基于契约的设计(DBC)
- 单元测试中指定契约