Liskov substitution principle (里氏替換原則)

背景

可替代性是针对对象程序设计中的一项原则,其指出在计算机程序中,如果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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值