背景
可替代性是针对对象程序设计中的一项原则,其指出在计算机程序中,如果S是T的子类型,则类型T的对象可以用类型S的对象替换(即类型T的对象可以用子类型S的任何对象替换(即向上转换))而不更改程序的任何期望属性(正确性,任务执行等)。
更正式地讲,Liskov替换原理(LSP)是子类型关系的特定定义,称为(强)行为子类型化,最初是由Barbara Liskov在1987年的大会主题演讲中约会的,主题为数据抽象和层次结构。这是一种语义关系,而双重句法关系,因为它遵循保证层次结构中类型的语义互操作性,Barbara Liskov和Jeannette Wing在1994年的文章中中简要地描述了这一原理
Subtype Requirement: Let ϕ ( x ) \phi (x) ϕ(x) be a property provable about objects x x x of type T T T . Then ϕ ( y ) \phi (y) ϕ(y) should be true for objects y y y of type S where S S S is a subtype of T T T.
当第一次学习针对对象的编程时,继承通常被描述为 “is a /是” 关系。
如果企鹅Penguin是鸟Bird,则Penguin类应从Bird类继承。通过 “ is a” 确定继承关系的技术既简单又有用,但偶尔会导致继承的错误使用。
里氏替代原则是一种确保正确使用继承的方法。
Penguin Problem
引起问题的“is a”技术的经典示例是 圆形椭圆问题(又称矩形正方形问题)。但是,我要使用 penguins 作为例子。
首先,考虑一个 application,该 application 显示鸟类以天空中飞来飞去。会有多种鸟类,因此,开发人员决定使用开放式封闭原则(Open Closed Principle . eg. OCP)来“封闭”代码,以添加新类型的鸟。为此,将创建以下抽象的Bird基类:
class Bird {
public:
virtual void setLocation(double longitude, double latitude) = 0;
virtual void setAltitude(double altitude) = 0;
virtual void draw() = 0;
};
BirdsFlyingAroundApp的第一个版本取得了巨大的成功。第二版轻松添加了另外12种不同类型的鸟类,也取得了成功。开放封闭原则万岁。但是,需要第三版应用程序才能支持企鹅。开发人员创建了一个新的Penguin类,该类继承自Bird类,但是存在一个问题:
void Penguin::setAltitude(double altitude)
{
//altitude can't be set because penguins can't fly
//this function does nothing
}
如果重写方法不执行任何操作或仅引发异常,则可能是在违反LSP。
运行该应用程序时,所有的飞行模式看起来都不正确,因为Penguin对象忽略了setAltitude方法。企鹅只是在地上乱跳。即使开发人员尝试遵循OCP,他们还是失败了。必须修改现有代码以适应Penguin类。
从技术上讲,企鹅是“鸟”,而“鸟”类则假设所有鸟都可以飞。由于企鹅子类违反了飞行假设,它不满足Bird父类的Liskov替代原理。
为什么违反LSP是错误的
使用抽象基类的主要目的是,以便将来可以编写新的子类并将其插入现有的,经过测试的代码中。这是开放封闭原则的本质。但是,当子类不能正确地遵循抽象基类的接口时,您不得不遍历现有代码并考虑涉及违法子类的特殊情况。这公然违反了开放封闭原则。
例如,看一下这段代码:
//Solution 1: The wrong way to do it
void ArrangeBirdInPattern(Bird* aBird)
{
Pengiun* aPenguin = dynamic_cast<Pengiun*>(aBird);
if(aPenguin)
ArrangeBirdOnGround(aPenguin);
else
ArrangeBirdInSky(aBird);
}
LSP表示代码应该在不知道Bird对象的实际类的情况下工作。如果您想添加另一种不会飞的鸟,例如Emu呢?然后,您必须遍历所有现有代码,并检查Bird指针是否实际上是Emu指针。您现在应该皱鼻子,因为空气中肯定有代码气味。
两种可能的解决方案
我们希望能够在不修改现有代码的情况下添加Penguin类。这可以通过修复不良继承层次结构使其满足LSP来实现。
解决问题的一种不太好的方法是向Bird类添加一个方法,该方法名为isFlightless。这样,至少可以添加其他不会飞的鸟类,而不会违反OCP。这将导致如下代码:
//Solution 2: An OK way to do it
void ArrangeBirdInPattern(Bird* aBird)
{
if(aBird->isFlightless())
ArrangeBirdOnGround(aBird);
else
ArrangeBirdInSky(aBird);
}
这确实是一个创可贴解决方案(band-aid solution)。它没有解决潜在的问题。它只是提供一种方法来检查特定对象是否存在问题。
更好的解决方案是确保不会飞的鸟类不会继承其父类的飞行功能。可以这样完成:
//Solution 3: Proper inheritance
class Bird {
public:
virtual void draw() = 0;
virtual void setLocation(double longitude, double latitude) = 0;
};
class FlightfulBird : public Bird {
public:
virtual void setAltitude(double altitude) = 0;
};
在上述解决方案中,Bird基类不包含任何飞行功能,而FlightfulBird子类添加了该功能。这允许将某些功能同时应用于Bird和FlightfulBird对象;但是,可能无法飞行的Bird对象不能推入使用FlightfulBird对象的函数中。
参考文献
tomdalling - SOLID Class Design: The Liskov Substitution Principle
wiki - Liskov_substitution_principle