在本讲中,我来为大家介绍一下软件设计原则里面的第二个原则,即里氏代换原则。
概述
首先,大家应该知道,里氏代换原则是面向对象设计的基本原则之一。那什么是里氏代换原则呢?里氏代换原则是指任何基类可以出现的地方,子类一定可以出现。这句话不好理解,但大家可以通俗理解成子类可以扩展父类的功能,但不能改变父类原有的功能。现在,这句话就好理解很多了,指的就是在Java里面通常都会有父子类的关系,一般而言,我们都会将子类中的功能抽取到父类中,以提高代码的复用性,而在子类中,我们只需要去定义子类特有的功能即可。
换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。为什么呢?因为如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性就会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。
你想啊,要是在父类中已经声明了一个方法,而你又在子类中再进行了一个重写,那么在父类中定义的方法是不是就没有任何意义了?如果说父类定义规则,要求子类必须重写,那么在父类中只需要定义成抽象的方法就可以了。
经过我上面的描述,相信大家对里氏代换原则有了一个简单的认识。接下来,我就为大家介绍里氏替换原则中的一个经典的案例,即正方形不是长方形。
案例
案例分析
在数学领域里,正方形毫无疑问是长方形,它是一个长宽相等的长方形。所以,如果我们要开发一个与几何图形相关的软件系统的话,那么就可以顺理成章的让正方形继承自长方形了。
请看一下下面这张类图。
可以看到,这张类图里面有三个类,第一个是长方形类,长方形类里面有两个成员变量,一个是length,表示长,一个是width,表示宽,而且它里面还提供了相应的getter和setter方法,相对来说,这个类还是很简单的,比较好理解。
第二个是长方形类的子类,即正方形类,该类要重写父类中设置长和宽的这两个方法。为什么要重写呢?因为正方形里面的长和宽是相等的。
以上两个类介绍完之后,再来看最后一个类,即测试类,在测试类中应该提供这么几个方法:
- 主方法:这里面我没有写出来
- resize方法:扩宽方法。长方形里面的宽是要比长小的,如果宽比长小的话,那么我们就可以通过该方法来进行判断,然后再将宽给它扩长,直到比长大就OK了
- 打印长和宽的方法:该方法只是为了更好的看到效果而已
注意了,在resize方法和打印长和宽的这两个方法里面,还需要传递一个长方形类型的参数,也就是说测试类其实是依赖于长方形类的,所以它俩之间是一个依赖关系。
把以上这三个类以及依赖关系理清楚了之后,接下来我们就要编写代码来实现这个案例了。
案例实现
打开咱们的maven工程,然后在com.meimeixia.principles包下创建一个子包,即demo2,接着在com.meimeixia.principles.demo2包下再创建一个子包,即before,我们首次是在该包下来存放咱们编写的代码的。接下来,我们就要正式开始编写代码来实现以上案例了。
首先,在com.meimeixia.principles.demo2.before包下新建第一个类,即长方形类,名字可取做Rectangle。
package com.meimeixia.principles.demo2.before;
public class Rectangle {
private double length;
private double width;
public double getLength() {
return length;
}
public void setLength(double length) {
this.length = length;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
}
然后,新建第二个类,即正方形类,名字可取做Square,记住要让该类去继承长方形类,并重写父类中设置长和宽的方法。那么应该如何去重写呢?很简单,就拿重写父类中设置长的setLength方法来说,我们只需要调用父类中的设置长和宽的方法把方法中的length参数设置给长和宽即可,因为长和宽必须保持一致。当然,重写父类中设置长的setWidth方法也是同理。
package com.meimeixia.principles.demo2.before;
public class Square extends Rectangle {
@Override
public void setLength(double length) {
super.setLength(length);
super.setWidth(length);
}
@Override
public void setWidth(double width) {
super.setLength(width);
super.setWidth(width);
}
}
接着,我们就要编写测试类了,名字就叫RectangleDemo。根据我们上面的分析,相信你一定能写出下面的代码,只不过现在还未在主方法中编写测试代码。
package com.meimeixia.principles.demo2.before;
public class RectangleDemo {
public static void main(String[] args) {
}
public static void resize(Rectangle rectangle) {
while (rectangle.getWidth() <= rectangle.getLength()) {
rectangle.setWidth(rectangle.getWidth() + 1);
}
}
public static void printLengthAndWidth(Rectangle rectangle) {
System.out.println(rectangle.getLength());
System.out.println(rectangle.getWidth());
}
}
紧接着,在主方法中编写如下代码进行测试。
package com.meimeixia.principles.demo2.before;
public class RectangleDemo {
public static void main(String[] args) {
Rectangle r = new Rectangle();
r.setLength(20);
r.setWidth(10);
resize(r);
printLengthAndWidth(r);
}
public static void resize(Rectangle rectangle) {
while (rectangle.getWidth() <= rectangle.getLength()) {
rectangle.setWidth(rectangle.getWidth() + 1);
}
}
public static void printLengthAndWidth(Rectangle rectangle) {
System.out.println(rectangle.getLength());
System.out.println(rectangle.getWidth());
}
}
这时,我们不妨来运行一下以上测试类,看看打印结果是啥?如下图所示,可以看到长是20,没变,宽是21,因为我们进行了一个扩宽的操作,此时,宽已经比长大了。
如果我像下面这样向resize方法中传入一个正方形类型的对象,那么可不可以呢?
package com.meimeixia.principles.demo2.before;
public class RectangleDemo {
public static void main(String[] args) {
Rectangle r = new Rectangle();
r.setLength(20);
r.setWidth(10);
resize(r);
printLengthAndWidth(r);
System.out.println("=============================");
Square s = new Square();
s.setLength(10);
resize(s);
printLengthAndWidth(s);
}
public static void resize(Rectangle rectangle) {
while (rectangle.getWidth() <= rectangle.getLength()) {
rectangle.setWidth(rectangle.getWidth() + 1);
}
}
public static void printLengthAndWidth(Rectangle rectangle) {
System.out.println(rectangle.getLength());
System.out.println(rectangle.getWidth());
}
}
从语法上来说是可以的,因为正方形是属于长方形的子类的,所以传递子类对象完全是可以的。
这时,我们再来运行一下测试类,看一下打印结果是什么,如下图所示,你会发现等了好久,结果却什么都没有打印出来。
难道是我们程序出现问题了吗?其实不是,你注意看以上控制台中的那个红色按钮,这表明程序还没有结束,它还在一直执行,为什么会出现这种现象呢?下面我就为大家解释一下其原因。
运行以上测试类中代码,你就会发现,假如我们把一个普通长方形对象作为参数传入resize方法中的话,是会看到长方形的宽度逐渐增长的效果的,当宽度大于长度时,代码就会停止,这种行为的结果符合我们的预期;假如我们再把一个正方形对象作为参数传入resize方法的话,就会看到正方形的宽度和长度都在不断增长,因为长和宽要保持一致,这是正方形的一个特点,那么代码就会一直运行下去,直至系统产生溢出错误为止。
所以,普通的长方形是适合这段代码的,而正方形不适合。而里氏代换原则又是指基类能使用的地方,那么子类也可以使用,因此很显然这违背了这一原则。
于是,我们可以得出这样一个结论:在resize方法中,Rectangle类型的参数是不能被其子类Square类型的参数所代替的,如果进行了替换就得不到预期结果。因此,Square类和Rectangle类之间的继承关系违反了里氏代换原则,它们之间的继承关系不成立,正方形不是长方形。
那么如何改进呢?下面我们再说。
案例改进
初步实现以上正方形不是长方形的案例之后,相信你也看到了其所在的问题,即违反了里氏代换原则。那么应该如何对该案例进行改进呢?
首先,我们要对类以及类和类之间的关系进行重新设计,重新设计出来的类图应该是下面这个样子的。
对于正方形和长方形而言,我们向上抽取,抽取出来一个四边形接口(即Quadrilateral),并在这个接口里面定义两个抽象的方法,一个是getLength,一个是getWidth,分别用于获取长和宽,然后让Rectangle类和Square类实现Quadrilateral接口;从以上类图中可以看到,我们还在Square类里面定义了一个名字为side的成员变量,也即正方形的边长,而且在该类里面,除了提供该成员变量的getter和setter方法之外,我们还重写了Quadrilateral接口里面的抽象方法;至于Rectangle类,依旧还是原先的设计,该类是没有任何变化的。
最后,大家不要忘了,还有一个测试类(即RectangleDemo),该测试类是没有成员变量的,只有如下三个方法:
- 主方法:这里面我没有声明出来,主要作测试用
- resize方法:扩宽方法。注意,该方法需要的是一个长方形类型的对象,正方形类型的对象此时是不能传入进来的
- printLengthAndWidth方法:打印长和宽的方法。注意,该方法需要传递的是一个Quadrilateral接口的子实现类对象
这样一路分析下来,你就会发现该测试类不仅得依赖Quadrilateral接口,还得依赖Rectangle类。至此,我就给大家分析完以上类图了,接下来,我们就得编写代码来实现以上改进后的案例了。
首先,在com.meimeixia.principles.demo2包下再创建一个子包,即after,该包下存放的就是改进后的案例的代码。
然后,我们再创建一个四边形接口。
package com.meimeixia.principles.demo2.after;
public interface Quadrilateral {
double getLength();
double getWidth();
}
接着,再来创建咱们的正方形类,注意了,该类是要去实现四边形接口的,这样,我们还必须得重写其中的方法。此外,在该类里面我们还得声明一个表示边长的成员变量,当然还得提供其对应的getter和setter方法。
package com.meimeixia.principles.demo2.after;
public class Square implements Quadrilateral {
private double side;
public double getSide() {
return side;
}
public void setSide(double side) {
this.side = side;
}
@Override
public double getLength() {
return side;
}
@Override
public double getWidth() {
return side;
}
}
紧接着,再来创建咱们的长方形类,同理,该类也得实现四边形接口,重写其里面的抽象方法。
package com.meimeixia.principles.demo2.after;
public class Rectangle implements Quadrilateral {
private double length;
private double width;
public void setLength(double length) {
this.length = length;
}
public void setWidth(double width) {
this.width = width;
}
@Override
public double getLength() {
return length;
}
@Override
public double getWidth() {
return width;
}
}
最后,我们再来创建一个测试类。根据我们上面对类图的分析,相信你一定能写出下面的代码。
package com.meimeixia.principles.demo2.after;
public class RectangleDemo {
public static void main(String[] args) {
Rectangle r = new Rectangle();
r.setLength(20);
r.setWidth(10);
resize(r);
printLengthAndWidth(r);
}
public static void resize(Rectangle rectangle) {
while (rectangle.getWidth() <= rectangle.getLength()) {
rectangle.setWidth(rectangle.getWidth() + 1);
}
}
public static void printLengthAndWidth(Quadrilateral quadrilateral) {
System.out.println(quadrilateral.getLength());
System.out.println(quadrilateral.getWidth());
}
}
此时,不妨来运行一下以上测试类,看看打印结果是不是我们所想要的,如下图所示,可以看到长是20,没变,宽是21,因为我们进行了一个扩宽的操作。
大家现在想一想,如果我们去调用resize方法时传入的是一个正方形对象,那么还可不可以呢?肯定是不可以的,因为正方形和长方形它俩现在没有父子关系了,所以在resize方法里面只能传递长方形对象,而不能再传递正方形对象了。这样,我们就通过以上改进完美的解决了案例之前所存在的问题。
|