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

什么是里氏替换原则

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

为什么需要里氏替换原则

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

我们定义了一个矩形类:

{private int width;private int height;(int width){this.width = width;System.out.println(“Rectangle width” + width);}(int height){this.height = height;System.out.println(“Rectangle height” + height);}}

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

{}

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

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

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

{(int height) {super.setWidth(height);super.setHeight(height);System.out.println(“Square height” + height);}(int width) {super.setWidth(width);super.setHeight(width);System.out.println(“Square width” + width);}}

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

(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 类添加计算面积的方法:

(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 同时实现该接口。

自然而然不想去因为别人的努力而努力,

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

相关文章:

你感兴趣的文章:

标签云: