里氏替换原则:切忌按照常识实现类间的继承关系

什么是里氏替换原则

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

为什么需要里氏替换原则

里氏替换原则看起来好像没啥了不起的,不就是继承要注意的一丢丢细节么,年轻人呐,你这样的思想很危险啊。事实上里氏替换原则常常会被违反,我在下面举例说明吧:

我们定义了一个矩形类:

public class Rectangle {
    private int width;
    private int height;

    public void setWidth(int width){
        this.width = width;

        System.out.println("Rectangle width" + width);
    }

    public void setHeight(int height){
        this.height = height;

        System.out.println("Rectangle height" + height);
    }
}

从数学知识来看,我们认为正方形是特殊的矩形(长宽相等),那么如果我们需要一个正方形类,一般都会把代码写成下面那样:

public class Square extends Rectangle{

}

大家有没有想到,本来正方形只需要边长的值就足够完成它的需求了,但是,由于 Square 继承于 Rectangle,那么 Square 类中必然拥有 width 和 height,即使我们在设置它们大小的时候让它们同时改变,但是 width 和 height 一定有一个是多余的。那么如果我们需要画成千上万个正方形的时候,就会产生成千上万个多余的 width 或 height。

此外,让 Square 继承 Rectangle 还会出现很奇怪的问题:由于里氏替换原则,只要是 Rectangle 类能出现的地方,Square 类必须也能出现,那么,任何对 Rectangle 类的对象进行 setWidth()/setHeight() 方法操作的地方,应该都能使用 Square 类的对象进行相同的操作。但是,Square 类明明长宽相等,为什么要进行同样的操作两次呢?

可能大家觉得这个例子说服力不够,那我再举一个例子来说明即使我们重写了 setWidth()/setHeight() 方法,仍然会存在问题:

public class Square extends Rectangle{
    @Override
    public void setHeight(int height) {
        super.setWidth(height);
        super.setHeight(height);

        System.out.println("Square height" + height);
    }

    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);

        System.out.println("Square width" + width);
    }
}

然后在操作 Square 和 Rectangle 的类中添加一个这样的方法:

    public void initRec(Rectangle r){
        r.setWidth(6);
        r.setHeight(10);
    }

由于里氏替换原则,我们当然可以将 Square 类对象传入这个方法,那么问题就来了,此时 Square 类对象的边长到底是哪一个呢?我们分别将 Rectangle 和 Square 传入该方法,看看实际的输出:

Rec

Rectangle width6

Rectangle height10

Squ

Rectangle width6

Rectangle height6

Square width6

Rectangle width10

Rectangle height10

Square height10

大家也会发现了,这个时候 Squ 的行为已经变得很奇怪了,它的边长到底是6还是10呢?当然了,要修复这个 Bug 很简单,但这不代表代码是没有问题的,因为为了修正这个错误,我们又得回去修改类,以符合实际的情况,不信的话再看下面的例子:

矩形能够计算面积很正常对吧?那我们就为 Rectangle 类添加计算面积的方法:

    public int getRecArea(Rectangle r){
        return r.getHeight()*r.getWidth();
    }

然后把这个方法放到 initRec() 方法里面执行,那么,当我们把 Square 对象传到 initRec() 方法内部时肯定没有问题,但是计算出来的面积肯定有问题,因为我们刚刚就说了,连 Square 对象的边长都确定不了,我们要怎么去确定它的面积呢?

问题到底出在哪?

大家到现在也许会发现,即使是这么简单的 Square 和 Rectangle 类间关系,都会让我们在维护过程中痛苦不已,不断地回去修改类内的代码,添加各种各样规避错误的逻辑。很多人就会觉得很奇怪了,这样写类应该是没有问题的啊,为什么会出现这样的错误啊?

实际上,问题的根源在于,在程序设计时,Square 类并不能被看作 Rectangle 类的子类,即使在数学上正方形就是特殊的矩形。因为 Square 类的行为和属性和 Rectangle 类的行为和属性是不一致的,将两个类的行为和属性进行抽象我们会发现两者根本不能达到一致:

Square 的属性只有边长,而 Rectangle 有 width 和 height

Square 只需要一个设置边长的方法,而 Rectangle 则需要两个 set 方法。

所以从这个例子中我们也能发现,在程序设计的过程中,进行类间继承关系的设计并不能按照常识去执行,而是需要从实际出发,从类的抽象行为、抽象属性出发,考虑类间的关系是否能成为一个 is-a 关系,如果子类 B 和父类 A 不能实现完全的 is-a 关系,那么我们能就不能进行继承。换句话说,如果类 B 中的某些实现又需要依赖类 A 的某些实现,那么我们就该考虑将这部分实现转到接口之中,让类 A 和类 B 同时实现该接口。

时间: 2024-11-07 17:43:20

里氏替换原则:切忌按照常识实现类间的继承关系的相关文章

"围观"设计模式(2)--里氏替换原则(LSP,Liskov Substitution Principle)

在面向对象的程序设计中,里氏替换原则(Liskov Substitution principle)是对子类型的特别定义.它由芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次会议上名为"数据的抽象与层次"的演说中首先提出. 里氏替换原则的内容可以描述为: "派生类(子类)对象能够替换其基类(超类)对象被使用." 以上内容并非利斯科夫的原文,而是译自罗伯特·马丁(Robert Martin)对原文的解读.其原文为: Let be a property

"围观"设计模式(2)--里氏替换原则(LSP,Liskov Substitution Principle)

在面向对象的程序设计中.里氏替换原则(Liskov Substitution principle)是对子类型的特别定义.它由芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次会议上名为"数据的抽象与层次"的演说中首先提出. 里氏替换原则的内容能够描写叙述为: "派生类(子类)对象能够替换其基类(超类)对象被使用." 以上内容并不是利斯科夫的原文,而是译自罗伯特·马丁(Robert Martin)对原文的解读. 其原文为: Let be a prope

六大原则之里氏替换原则

阐述一下: 肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型. 定义2:所有引用基类的地方必须能透明地使用其子类的对象. 问题由来:有一功能P1,由类

设计模式原则之里氏替换原则

里氏替换原则,OCP作为OO的高层原则,主张使用“抽象(Abstraction)”和“多态(Polymorphism)”将设计中的静态结构改为动态结构,维持设计的封闭性.“抽象”是语言提供的功能.“多态”由继承语义实现. 定义1:如果对每一个类型为T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型. 定义2:所有引用基类的地方必须能透明地使用其子类的对象. 如

设计模式六大原则(2):里氏替换原则

里氏替换原则 肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型. 定义2:所有引用基类的地方必须能透明地使用其子类的对象. 问题由来:有一功能P1,由

设计模式六大原则(2):里氏替换原则(转载)

肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型. 定义2:所有引用基类的地方必须能透明地使用其子类的对象. 问题由来:有一功能P1,由类A完成.现需

设计模式六大原则(2):里氏替换原则(Liskov Substitution Principle)

肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型. 定义2:所有引用基类的地方必须能透明地使用其子类的对象. 问题由来:有一功能P1,由类A完成.现需

设计模式六大原则之二:里氏替换原则

定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型. 定义2:所有引用基类的地方必须能透明地使用其子类的对象. 问题由来:有一功能P1,由类A完成.现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成.新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障. 解决方

[转]设计模式六大原则[2]:里氏替换原则

肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型. 定义2:所有引用基类的地方必须能透明地使用其子类的对象. 问题由来:有一功能P1,由类A完成.现需