从零开始学习Java设计模式 | 软件设计原则篇:里氏代换原则

Posted 李阿昀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始学习Java设计模式 | 软件设计原则篇:里氏代换原则相关的知识,希望对你有一定的参考价值。

在本讲中,我来为大家介绍一下软件设计原则里面的第二个原则,即里氏代换原则。

概述

首先,大家应该知道,里氏代换原则是面向对象设计的基本原则之一。那什么是里氏代换原则呢?里氏代换原则是指任何基类可以出现的地方,子类一定可以出现。这句话不好理解,但大家可以通俗理解成子类可以扩展父类的功能,但不能改变父类原有的功能。现在,这句话就好理解很多了,指的就是在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;

/**
 * 长方形类
 * @author liayun
 * @create 2021-05-27 13:26
 */
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;

/**
 * 正方形类
 * @author liayun
 * @create 2021-05-27 13:32
 */
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;

/**
 * @author liayun
 * @create 2021-05-27 13:42
 */
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;

/**
 * @author liayun
 * @create 2021-05-27 13:42
 */
public class RectangleDemo 

    public static void main(String[] args) 
        // 创建长方形对象
        Rectangle r = new Rectangle();
        // 设置长和宽
        r.setLength(20);
        r.setWidth(10);
        // 调用resize方法进行扩宽
        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;

/**
 * @author liayun
 * @create 2021-05-27 13:42
 */
public class RectangleDemo 

    public static void main(String[] args) 
        // 创建长方形对象
        Rectangle r = new Rectangle();
        // 设置长和宽
        r.setLength(20);
        r.setWidth(10);
        // 调用resize方法进行扩宽
        resize(r);
        printLengthAndWidth(r);

        System.out.println("=============================");
        // 创建正方形对象
        Square s = new Square();
        // 设置长和宽
        s.setLength(10);
        // 调用resize方法进行扩宽
        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;

/**
 * 四边形接口
 * @author liayun
 * @create 2021-05-27 14:27
 */
public interface Quadrilateral 

    // 获取长
    double getLength();

    // 获取宽
    double getWidth();


接着,再来创建咱们的正方形类,注意了,该类是要去实现四边形接口的,这样,我们还必须得重写其中的方法。此外,在该类里面我们还得声明一个表示边长的成员变量,当然还得提供其对应的getter和setter方法。

package com.meimeixia.principles.demo2.after;

/**
 * 正方形类
 * @author liayun
 * @create 2021-05-27 14:32
 */
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;

/**
 * 长方形类
 * @author liayun
 * @create 2021-05-27 14:37
 */
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;

/**
 * @author liayun
 * @create 2021-05-27 14:49
 */
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方法里面只能传递长方形对象,而不能再传递正方形对象了。这样,我们就通过以上改进完美的解决了案例之前所存在的问题。

以上是关于从零开始学习Java设计模式 | 软件设计原则篇:里氏代换原则的主要内容,如果未能解决你的问题,请参考以下文章

从零开始学习Java设计模式 | 软件设计原则篇:里氏代换原则

从零开始学习Java设计模式 | 软件设计原则篇:里氏代换原则

从零开始学习Java设计模式 | 软件设计原则篇:合成复用原则

从零开始学习Java设计模式 | 软件设计原则篇:合成复用原则

从零开始学习Java设计模式 | 软件设计原则篇:依赖倒转原则

从零开始学习Java设计模式 | 软件设计原则篇:依赖倒转原则