设计模式六大原则——里氏替换原则
概念
里氏替换原则(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;
}