里氏替换原则以及IS-A关系的判定

本文探讨了里氏替换原则在面向对象编程中的重要性,指出子类应能替换基类并保持程序行为正确。通过举例说明,作者揭示了仅依据现实世界的IS-A关系来决定继承关系可能导致的问题,强调应根据类的行为来判断继承是否合适。错误的继承关系可能会违反里氏替换原则,导致代码质量下降和维护困难。

        相信许多人都看过GoF的设计模式以及各种设计模式的书籍。这些繁复的设计模式究竟是为了什么呢?这些设计模式为的是维护一组原则,这组原则使得代码灵活,可维护,远离所谓的“代码臭味”。

        这组原则就是 SOLID

        Single Responsibility Principle 单一职责原则

        Open-Close Principle 开闭原则

        Liskov Substitution Principle 里氏替换原则

        Interface Segregation Principle 接口隔离原则

        Dependency Inversion Principle 依赖倒置原则

        这篇文章谈论的,里氏替换原则,一句话描述这个原则就是“子类可以替换基类”。

        非常简单,非常容易理解,稍微了解过面向对象编程的继承概念就可以领会这个原则谈论的是什么。初看之下似乎是自然而然的事情。

        但程序的困难性和复杂度完全来自于现实,在现实中,才会明白实践这个看起来自然而然的原则是有难度的。

        大部分谈论具有对象的语言的书,在对于如何设计继承关系,都会提到“IS-A”判断法,这其实就是里氏替换原则,子类IS-A基类,这样继承关系才得以成立。

        举个例子:

    

class human {
    public:
    virtual void say() {
        cout << "I am human";
    }
};

class worker: public human {
    public:
    virtual void say() {
        cout << "I am worker";
    } 
};

void let_a_human_say_something(shared_ptr<human> p) {
    p->say();
}

        let_a_human_say_something中,需要一个human,并且调用其say方法来打印一些信息,而如果我们传入worker的指针其实是没问题的,因为worker也拥有say方法,打印相应信息。

        在这个很简单的情形下,worker和human的设计其实是符合里氏替换原则的,因为两个类的指针都可以被传给let_a_human_say_something,让其工作。

        但是一个误区是认为IS-A就是和现实关系对应,即现实中某个类属于某个更大范畴的类,那么他们存在一个继承关系。在上面所说的例子中,worker is human, 符合常识。但是一旦进入实际的软件开发实践中,我们很快就会发现有所不同。

      下面我们看一个《敏捷软件开发:原则,模式与实践》里列出的例子。

        

class triangle {
    public:
    virtual void set_height(int h) {
        ...
    }
    virtual void set_width(int w) {
        ...
    }
    virtual int get_area() {
        ...
    }
};

class square: public triangle {
    public:
    virtual void set_height(int h) {
        ...
    }
    virtual void set_width(int w) {
        ...
    }
    virtual int get_area() {
        ...
    }
};

        triangle代表矩形,它有三个方法,设置高,设置宽,计算面积。我们可以设置height和width,之后得出面积,我们可以为这个class做一系列功能,一系列测试。。。

        但是,现在我们想要一个square类,代表正方形。使用小学数学知识,你很快决定,让square类继承triangle类,因为这太明显了不是吗?正方形是一个矩形,这是一个数学上的事实, 正方形只不过是高和宽相等的矩形而已!square IS-A triangle!

        然而问题来了,你要如何让square类具有宽与高相等的性质呢?

        方案1: set_width和set_height都设置同一个变量

        方案2:  让set_width和set_height其中一个无效

        方案3:计算面积时只计入width或者height

        ...

        你可以灵活地想出不少方案,他们都是为了保证这个正方形的性质,你可以轻松的写出来,然后就万事大吉了对吧?

        如果我说在这个例子里你这样做就违背了里氏替换原则呢?

        我们来看一段代码

void test_triangle(triange* t) {
    t->set_width(5);
    t->set_height(6);
    assert(t->get_area() == 5* 6);
}

    这是一个简单的测试,然后很遗憾的,我们的square无论采用上面何种方案都无法通过。因为triangle类中,存在了一些假定,比如宽和高是独立的,面积等于宽乘以高。而继承自triangle的square无论如何去打补丁都会违背这种假定。让square继承triangle一定会打破这些假设,从而违背里氏替换原则。

        但是正方形确实是矩形啊,这是哪里出错了? 这是因为IS-A是对于类的行为的判断。也就是说只有一个东西走路像鸭子,叫起来像鸭子,我们才可以认为它同鸭子有继承关系。

        square在triangle定义的一系列行为中实际上并不像triangle,也就是说,在这一组行为/方法下,square不是triangle。

        只通过现实概念的思考而不去实际考察代码中类的行为/方法就确定继承关系,注定是会有悲剧发生的。

        你会发现父类有些方法在子类应该禁止调用,有些方法调用某个父类可以正常工作,但是调用子类就爆炸了,然而你看看类的名字似乎继承关系又是理所当然。这种情况其实就是里氏替换原则被隐秘地违背了。如果不警惕,代码将会走向僵死。

<think>好的,我现在需要回答用户关于里氏替换原则(Liskov Substitution Principle,LSP)中“is”判断条件的详细说明。首先,我得回忆一下LSP的基本概念。里氏替换原则是面向对象设计中的SOLID原则之一,由Barbara Liskov提出,主要强调子类必须能够替换它们的父类而不影响程序的正确性。 用户特别提到了“is”判断条件,这让我想到在面向对象编程中,我们常常用“is-a”关系来描述继承。例如,如果Dog继承自Animal,我们说Dog“is-a”Animal。但LSP指出,仅仅满足语法上的继承关系(即子类拥有父类的方法和属性)是不够的,还需要在行为上保持一致。也就是说,子类不应该改变父类原有的行为预期。 接下来,我需要具体解释“is”条件在LSP中的含义。根据引用[^1],LSP要求子类对象必须能够替换父类对象,而不会引发错误或逻辑问题。因此,“is”条件不仅仅是类型检查,还包括行为兼容性。例如,如果一个函数期望父类对象作为参数,传入子类对象时,函数的行为不应被破坏。 然后,我需要举例子来说明。比如,正方形继承自矩形的情况。从数学上说,正方形是矩形,但在编程中,如果修改正方形的高度会影响宽度,这违反了矩形的行为预期。因此,这样的继承关系符合LSP,因为子类改变了父类的行为。 用户可能还关心如何检测和避免违反LSP的情况。这时候可以提到设计时通过契约(前置条件、后置条件、不变式)来确保子类遵守父类的约束。例如,子类的前置条件不能强于父类,后置条件不能弱于父类,这样才能保证替换时的正确性。 最后,结合引用[^2]中关于代码自解释性的建议,强调通过清晰的代码设计和文档来维护LSP,减少不必要的注释,使代码本身表达出符合原则的结构。例如,通过合理的抽象和接口设计,避免子类覆盖父类方法时引入不兼容的行为。 总结起来,我需要将上述思路组织成结构化的回答,确保涵盖LSP中的“is”条件,解释其重要性,并通过实例和设计建议来加强理解,同时引用提供的参考资料来支持观点。</think>### 里氏替换原则中的“is”判断条件解释 在面向对象编程中,**里氏替换原则(Liskov Substitution Principle, LSP)** 是SOLID原则的核心组成部分之一。其核心思想是:**子类对象必须能够替换其父类对象,而不会破坏程序的逻辑正确性**[^1]。关于“is”判断条件,具体解释如下: --- #### 1. “is”条件的本质:行为兼容性 在面向对象设计中,“is-a”关系通常用于描述继承关系(例如“Dog is an Animal”)。但LSP强调,**语法上的继承关系is-a)必须伴随语义上的行为兼容性**。具体来说: - **子类不能违反父类的行为约定**:例如,若父类方法声明返回正整数,子类实现也必须满足这一约束。 - **子类的前置条件不能强于父类**(即不能要求更严格的输入条件)。 - **子类的后置条件不能弱于父类**(即不能承诺更宽泛的输出结果)。 例如,若父类方法`calculate(x)`要求`x > 0`,则子类方法不能要求`x > 10`,否则调用方可能因输入`x=5`而意外失败。 --- #### 2. 违反“is”条件的典型案例 **正方形与矩形的继承问题**: - 数学上,正方形是矩形(满足“is-a”关系)。 - 但在编程中,若通过继承实现: ```java class Rectangle { void setWidth(int w) { ... } void setHeight(int h) { ... } } class Square extends Rectangle { void setWidth(int w) { this.width = w; this.height = w; // 强制宽高相等,违反矩形行为约定! } } ``` 当用户调用`setWidth()`时,期望仅修改宽度,但子类却同时修改高度。这导致**父类与子类行为不一致**,违反了LSP。 --- #### 3. 如何满足“is”条件? - **基于契约设计(Design by Contract)**:明确父类方法的**前置条件**、**后置条件**和**不变式**,子类需遵守这些约束。 - **避免重写父类非抽象方法**:若子类需要修改父类方法的行为,可能暗示继承关系不合理。 - **优先使用组合而非继承**:若无法保证行为兼容性,可通过组合实现代码复用。 --- #### 4. 引用与扩展 Steve McConnell在《代码大全》中提到:**“好的代码是自身最好的文档”**。在设计符合LSP的代码时,应通过清晰的接口定义和自解释的命名(如`ImmutableList`替代`List`),减少对注释的依赖,同时保证行为一致性。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值