设计模式六大原则——里氏替换原则

设计模式六大原则——里氏替换原则

概念

里氏替换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏替换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。

问题描述

       一个功能T有类A来完成,后来由于需求的变化,该功能T被分为了T1和T2两部分,这两部分的功能分别有类A的子类:类B和类C来完成。如果功能T1发生了变化,修改类B的同事,有可能引起T2的功能产生故障。

产生原因

       在继承关系中,基类的存在是为整个继承的结构设定一系列的规定和约束,让整个结构都按照这个规定和约束来。例如说用一个基类来描述鸟类,根据我们对鸟类的一贯认知,会在基类中通过约定有羽毛属性,有飞翔行为的是鸟类。这样在实现布谷鸟或者杜鹃鸟的时候,它都有基类中规定的属性和行为约束,但是突然有一天boss过来说把企鹅也要加进来,因为企鹅也属于鸟类。此时我们在继承了鸟类这个基类的时候,把羽毛属性和飞翔的行为都改了。此时布谷鸟或者杜鹃鸟就都如企鹅一般没了羽毛,并且不会飞翔了。

解决办法

       当使用继承的时候,使用里氏替换原则。当使用继承的时候,尽量不覆盖或重写父类的方法。当扩展父类方法的时候,保证不影响父类功能的前提下扩展。

实例

用一个类来描述猫的叫声。

class Cat
{
public:
  void Say()
  {
    cout << "miao~" << endl;
  }
};

后来猫群里出现了一只很高冷的猫,它从来都不叫。这时候我们需要继承扩展Cat这个类了。具体实现如下:

class ReservedCat : public Cat
{
public:
  void Say()
  {
    cout << "Be quite..."  << endl;
  }
}

       根据里氏替换原则:任何出现基类的地方,都可以用子类替换。那么此刻就尴尬了,但凡有C++基础的同学都知道了,如果用里氏替换原则将它替换,那么所有的猫都变成高冷的猫了。这显然是不合理的,但是这种问题在实际应用中确实很常见的,难道说因为里氏替换原则,我学了的C++继承,多态什么的都不要了吗? 我用了继承之后代码就会有这么写个问题吗?这时候我们就有疑问了:我们如何去度量继承关系的质量?

Liskov于1987年提出了一个关于继承的原则“Inheritance should ensure that any property proved about supertype objects also holds for subtype objects.”——“继承必须确保超类所拥有的性质在子类中仍然成立。”也就是说,当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系。

       有的人(其实也包括我在内)就会这么想,如果用里氏替换原则来判断一个类的框架是否合理的话,那么C++(别的语言我不太清楚)里面的继承和多态是不是就没用了?答案显然是否定的。就上面的猫的这个例子来看,喜欢叫的猫和高冷的猫显然不应该是继承关系,而是并行的关系。在处理这种情况的时候,我们只需要定义一个共同的基类,创建一个纯虚函数来实现。那么假如我们非要用到继承来实现一个框架的时候怎么办呢?此时就要遵守里氏替换原则的四层含义:

  • 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
  • 子类中可以增加自己特有的方法。
  • 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

       总结起来就是:子类实现父类的抽象方法优先,但是不能覆盖父类的抽象方法。但是当子类必须要实现父类的方法的时候,那么就要遵守里氏替换原则中的第三条和第四条。

最后用设计模式之禅上的例子说明总结:

#include <iostream>  

using namespace std;  

//定义两个空类型用于实验  
class Shape  
{  
};  

class Rectangle : public Shape  
{  

};  
//C++中的抽象类就相当于java中的接口实现  
//C++中普通的父类(带有虚函数的,抽象方法)相当于java中的抽象类  
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一律被隐藏。  

    //子类的形参类型比父类更严格,  
    void drawShape(Rectangle r)  
    {  
        printf("Son:drawShape(Rectangle r)\n");  
    }

    //子类的形参类型比父类严宽松:表示的是父类  
    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;  
    cin.get();  
    return 0;  
}  
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值