第2章 面向对象的设计原则(SOLID):2_里氏替换原则(LSP)

2. 里氏替换原则(Liskov Substitution Principle,LSP)

2.1 定义

(1)所有使用基类的地方必须能透明地使用子类替换,而程序的行为没有任何变化(不会产生运行结果错误或异常)。只有这样,父类才能被真正复用,而且子类也能够在父类的基础上增加新的行为。也只有这样才能正确的实现多态

(2)当一个类继承了另一个类时,子类就拥有了父类中可以继承下来的属性和操作。但如果子类覆盖了父类的某些方法,那么原来使用父类的地方就可能会出现错误,因为表面上看,它调用了父类的方法,但实际运行时却调用了被子类覆盖的方法,而这两个方法的实现可能不一样,这就不符合LSP原则。(见后面的解决方案)

(3)里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。

【编程实验】正方形与长形的驳论

 //1、正方形是一种特殊的长方形(is - a关系)?

#include <stdio.h>

//长方形类
class Rectangle
{
protected:
    long width;
    long height;
public:
    void setWidth(long width){this->width = width;}
    long getWidth(){return this->width;}
    
    void setHeight(long height){this->height = height;}
    long getHeight(){return this->height;}
    
    long getArea(){return width * height;}
};

//正方形类(如果继承自长方形类)
class Square : public Rectangle
{
public:
    void setWidth(long width)
    {
        this->width = width;
        this->height = width;
    }
    
    long getWidth(){return this->width;}
    
    void setHeight(long height)
    {
        this->width = height;
        this->height = height;       
    }
    
    long getHeight(){return this->height;}      
};

int main()
{
    //LSP原则:父类出现的地方必须能用子类替换
    Rectangle* r = new Rectangle();//Square *r = new Square();
    
    r->setWidth(5);
    r->setHeight(4);
    
    printf("Area = %d\n",r->getArea()); //当用子类时,结果是16。用户就不
                                       //明白为什么长5,宽4的结果不是20,而是16.
                                       //所以正方形不能代替长方形。即正方形不能
                                       //继承自长方形的子类
    return 0;
}

 //2. 改进的继承关系——符合LSP原则

#include <stdio.h>

//抽象的四方形类
class QuadRangle
{   
public:
    //将四方形抽象出公共部分出来
    virtual long getArea() = 0;     //面积
    virtual long getPerimeter() = 0;//周长
};

//长方形类(继承自抽象的四方形类)
class Rectangle : public QuadRangle
{
private:
    long width;
    long height;
public:
    Rectangle(long width, long heigth)
    {
        this->width = width;
        this->height = heigth;
    }
    
    void setWidth(long width){this->width = width;}
    long getWidth(){return this->width;}
    
    void setHeight(long height){this->height = height;}
    long getHeight(){return this->height;}
    
    long getArea(){return width * height;}
    long getPerimeter(){return (width + height) * 2;}
};

//正方形类(继承自抽象的四方形类)
class Square : public QuadRangle
{
    long side;
public:
    Square(long side) {this->side = side;}
    
    void setSide(long side);
    long getSide(){return this->side;}
    long getPerimeter(){return 4 * side;}
    long getArea(){return side * side;}
};

int main()
{
    //LSP原则:父类出现的地方必须能用子类替换
    QuadRangle* q = new Rectangle(5, 4); //Rectangle* q = new Rectangle(5, 4);或Square *q = new Square(5);
       
    printf("Area = %d, Perimeter = %d\n",q->getArea(), q->getPerimeter()); 
    
    return 0;
}

2.2 LSP原则的4层含义

(1)子类必须实现父类中声明的所有方法。

  ①步枪、手枪和机关枪都继承于AbstractGun,因此都实现了shoot(射击)的功能。

  ②玩具枪不能直接继承于AbstractGun。因为玩具枪不能去实现父类的shoot功能(即子类不能完全实现父类的方法,违反LSP原则),否则这样的武器拿给士兵去杀敌会闹笑话。因此,ToyGun不能继承于AbstractGun,而是继承于AbstracToy,然后去仿真枪的行为。这样对于士兵类来讲,因要求传入的是AbstactGun类的对象,所以不能使用玩具手枪杀人。

(2)子类可以扩展功能,但不能改变父类原有的功能

  ①子类可以有自己的属性和操作。因此,里氏替换原则只能正着用,不能返过来用。即子类出现的地方,父类未必就可以替换。如Snipper类的killEnemy方法中不能传入Rifle类的对象,因为Rifle类中没有zoomOut的方法。

  ②父类向下转换是不安全的,可能会调用到只有在子类中出现的方法而造出异常。

(3)子类可以实现父类的抽象方法,但一般不要覆盖父类的非抽象方法。

4)如果覆盖或实现父类方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。方法的后置条件(即方法的返回值)要比父类更严格

  ①子类只能使用相等或更宽松的前置条件来替换父类的前置条件。当相等时表示覆盖,不同时表示重载。

  为什么只能放大?因为父类方法的参数类型相对较小,所以当传入父类方法的参数类型(或更窄类型)时,重载时将优先匹配父类的方法,而子类的重载方法不会匹配,因此保证了仍执行父类的方法,所以业务逻辑不变(对于C++而言,父子类之间的同名函数发生隐藏而不是重载,因父类的函数被隐藏,当用子类替换父类时,永远调用不到父类的函数,LSP将无法被遵守)。若是覆盖时,必须清楚其逻辑要义,因为覆盖时子类的方法会被执行)

  ②只能使用相等或更强的后置条件来替换父类的后置条件。即返回值应该是父类返回值的子类或更小

  如果是重载,由于前置条件的要求,会调用到父类的函数,因此子类函数不会被调用

  如果是覆盖,则调用子类的函数,这时子类的返回值(S类型)比父类要求的小(T类型),这是被允许的,因为父类调用函数的时候,返回值至少是T类型,而子类的返回值S(类型小),给T类型的变量赋值是合法的。

  Father F = ClassF.Func();//;用子类替换时Father F = ClassC.Func()是合法的

【编程实验】前置条件和后置条件

#include <stdio.h>

class Shape
{  
};

class Rectangle : public Shape
{
    
};

class Father
{
public:
    virtual void drawShape(Shape s) //
    {
        printf("Father:drawShape(Shape s)\n");
    }
    
    virtual void showShape(Rectangle r) //
    {
        printf("Father:ShowShape(Rectangle r)\n");       
    }
    
    Shape CreateShape()
    {
        Shape s;
        printf("Father: Shape CreateShape()");
        return s;
    }
};

class Son : public Father
{
public:

    //对于C++而言,重载只能发生在同一作用域。显示Son和Father是不同作用域
    //所以,下面发生的是隐藏,而不是重载!因此,当使用子类时,不管下列
    //函数中的形参是否比父类更严格,只要同名,父类virtual一律被隐藏。

    //子类的形参类型比父类更严格
    virtual void drawShape(Rectangle r)  
    {
        printf("Son:drawShape(Rectangle r)\n");        
    } 
    
    //子类的形参类型比父类严宽松
    virtual void showShape(Shape s)
    {
        printf("Son:showShape(Shape s)\n");        
    }   

    //返回值类型比父类严格
    Rectangle CreateShape()
    {
        Rectangle r;
        printf("Son: Rectangle CreateShape()");
        
        return r;
    } 
};

int main()
{
    //当遵循LSP原则时,使用父类地方都可以用子类替换

    //Father* f = new Father(); //该行可用子类替换    
    Son* f = new Son(); //用子类替换父类出现的地方

    Rectangle r;
    
    //子类形参类型更严格时,下一行输出结果会发生变化,不符合LSP原则
    f->drawShape(r); //Father类型的f时,调用父类的drawShape(Shape s)
                     //Son类型的f时,发生隐藏,会匹配子类的drawShape
    
    //子类形参类型更宽松时,对于C++而言,会因发生隐藏而不符合LSP原则。但Java发生重载,会符合LSP
    f->showShape(r); //Father类型的f时,直接匹配父类的showShape(Rectangle r)
                     //Son类型的f时,因发生隐藏,会匹配子类的showShape(Shape s)
                  
    //子类的返回值类型更严格
    Shape s = f->CreateShape(); //替换为子类时,返回值为Rectangle,比Shape类型小,这种赋值是合法的
    
    delete f;
    
    return 0;
}

猜你喜欢

转载自blog.csdn.net/CherishPrecious/article/details/83854862