李氏替换原则(Liskov Substitution Principle: LSP)

子类型必须能够替换掉他们的父类型

这里的所有观点摘抄自《敏捷软件开发原则、模式与实践》,原著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(s).Draw();
    else if (s.itsType == Shape::circle)
        static_cast(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)
  • 单元测试中指定契约

你可能感兴趣的:(李氏替换原则(Liskov Substitution Principle: LSP))