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

Posted 李阿昀

tags:

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

在本讲,我将为大家介绍软件设计原则里面的第三个原则,即依赖倒转原则。

概述

什么是依赖倒转原则呢?我们来看一下下面这段描述:

高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

这句话看起来就不好懂,不过没关系,下面我会为大家详细解释下。

你可能会问的第一个问题是高层模块是什么?低层模块又是什么?下面我用一张图来解释一下。

仔细看完以上类图,相信你对高层模块和低层模块有了一个简单的认识。

继续看上面那句话,高层模块和低层模块两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。这句话又该如何理解呢?其实这里面的细节指的就是具体的实现类或者子类。以上面的例子来说,A类是依赖于B类的,而B类又是一个具体的类,也即不是抽象类或者接口。现在我们的做法就是要把B类向上进行抽取,抽取出来一个抽象类或者接口,那么这样的话,我们在进行依赖的时候,A类就不需要直接依赖于B类了,而是依赖其父类或者父接口就行了,即细节应该依赖于抽象。

看完以上那句话,如果大家还不是特别理解的话,那么接下来我再通过一个例子来为大家解释一下依赖倒转原则。其实,依赖倒转原则是开闭原则的具体的实现。

案例

这个例子就是组装电脑。

案例分析

现在我们要组装一台电脑,那么就得需要一些配件了,例如cpu、硬盘、内存条。只有这些配件都有了,计算机才能正常的运行。选择cpu的话,它又有很多品牌,比如说Intel、AMD等等,硬盘也是一样,它也有很多品牌,如希捷、西数等等,内存条同样也是,有金士顿、海盗船等等这些品牌。所以,我们在组装电脑的时候,可以选择任意一个品牌的配件来进行组装。

这样,通过对以上案例的描述,相信大家不难画出如下类图,要是真画不出来,那就看下面类图吧!

从以上类图中可以看到,有一个希捷硬盘类,它里面既有存储数据的方法,也有获取数据的方法;其次,还有一个英特尔cpu类,它里面有一个运行的方法,即run方法;最后还有一个金士顿内存条类,它里面有保存数据的方法。

大家注意了,以上三个类均不能独立运行,而是必须组合到Computer类里面,所以,你会看到在Computer类里面是以成员变量的形式来声明希捷硬盘、英特尔cpu、金士顿内存条等等这些配件的,继而我们就得为这些成员变量提供相应的getter和setter方法了。此外,在Computer类里面还定义了一个run方法,该方法就是用来运行计算机的,当然了,计算机的运行肯定是包含了希捷硬盘、英特尔cpu、金士顿内存条等等这些配件的运行的。

接下来,我们便来通过代码实现以上组装电脑案例。

案例实现

打开咱们的maven工程,然后在com.meimeixia.principles包下创建一个子包,即demo3,接着在com.meimeixia.principles.demo3包下再创建一个子包,即before,我们首次是在该包下来存放咱们编写的代码的。接下来,我们就要正式开始编写代码来实现以上案例了。

首先,在com.meimeixia.principles.demo3.before包下新建第一个类,即希捷硬盘类。

package com.meimeixia.principles.demo3.before;

/**
 * 希捷硬盘类
 * @author liayun
 * @create 2021-05-27 19:28
 */
public class XiJieHardDisk 

    // 存储数据的方法
    public void save(String data) 
        System.out.println("使用希捷硬盘存储数据为:" + data);
    

    // 获取数据的方法
    public String get() 
        System.out.println("使用希捷硬盘获取数据");
        return "数据";
    


然后,新建第二个类,即InterCpu。

package com.meimeixia.principles.demo3.before;

/**
 * Intel cpu
 * @author liayun
 * @create 2021-05-27 19:33
 */
public class IntelCpu 

    public void run() 
        System.out.println("使用Intel处理器");
    


接着,新建第三个类,即金士顿内存条类。

package com.meimeixia.principles.demo3.before;

/**
 * 金士顿内存条类
 * @author liayun
 * @create 2021-05-27 19:36
 */
public class KingstonMemory 

    public void save() 
        System.out.println("使用金士顿内存条");
    


至此,以上三个配件类我们就定义完毕了。而且在上面我也说过了,这些组件是不能独立去运行的,所以,我们还需要定义一个Computer类,并将以上三个配件给组合进来。

package com.meimeixia.principles.demo3.before;

/**
 * @author liayun
 * @create 2021-05-27 19:44
 */
public class Computer 

    private XiJieHardDisk hardDisk;
    private IntelCpu cpu;
    private KingstonMemory memory;

    public XiJieHardDisk getHardDisk() 
        return hardDisk;
    

    public void setHardDisk(XiJieHardDisk hardDisk) 
        this.hardDisk = hardDisk;
    

    public IntelCpu getCpu() 
        return cpu;
    

    public void setCpu(IntelCpu cpu) 
        this.cpu = cpu;
    

    public KingstonMemory getMemory() 
        return memory;
    

    public void setMemory(KingstonMemory memory) 
        this.memory = memory;
    

    public void run() 
        System.out.println("运行计算机");
        // 计算机运行时,各个配件应各司其职,干自己的活
        String data = hardDisk.get();
        System.out.println("从硬盘上获取的数据是:" + data);
        cpu.run();
        memory.save();
    


这时,我们就来运行一下以上测试类,看一下计算机能不能正常的去运行,并且使用硬盘、cpu和内存条等等这些配件。从下图所示的打印结果中可以看到,计算机是能正常运行的,并且还伴随着硬盘、cpu和内存条等等这些组件各司其职,做着自己的工作。

那么你觉得以上代码有没有什么问题啊?从以上代码中可以看到,已经组装了一台计算机,但是似乎组装的计算机的cpu只能是Intel的,内存条只能是金士顿的,硬盘只能是希捷的,为什么呢?因为在Computer类里面成员变量声明的是固定品牌的配件,如果我们后期想换成其他品牌的配件的话,那么是不是还得需要去修改Computer类啊?这就违背开闭原则了。

因此,这对用户肯定是不友好的,用户有了机箱之后,肯定是想按照自己的喜好选择自己喜欢的配件进行组装。

综上所述,以上案例遇到的问题就是,违背了开闭原则,因为后期如果我们想要把cpu换成AMD品牌的,那么我们还得去修改Computer类。

案例改进

虽说我们实现了以上组装电脑的案例,但是我们也看到了该案例所存在的问题,即违反了开闭原则。因此,我们就要对该案例进行改进了,那如何进行改进呢?这里就需要用到依赖倒转原则了。

首先,我们得重新设计类图。那如何设计新的类图呢?根据依赖倒转原则设计,即分别对希捷硬盘类、英特尔cpu类、金士顿内存条类向上抽取,抽取出一个父接口,完成之后,现在在Computer类里面就不再是聚合具体的实现类了,而是改成了聚合抽象接口这种方式。

那么,这样设计有什么好处呢?后期如果有其他品牌配件的话,例如西数品牌的硬盘,那么我们就只需要定义一个西数硬盘类,让它去实现HardDisk接口即可。这样,当我们在组装计算机的时候,就只需要创建西数硬盘类的对象并把它传递进来就OK了,而且Computer类还不需要进行修改哟!如此一来,以上组装电脑案例所存在的问题不就迎刃而解了吗?

下面,我们就来编写代码来改进一下以上案例。

首先,在com.meimeixia.principles.demo3包下再创建一个子包,即after,该包下存放的就是改进后的案例的代码。

然后,定义第一个接口,即HardDisk接口。

package com.meimeixia.principles.demo3.after;

/**
 * 硬盘接口
 * @author liayun
 * @create 2021-05-27 20:10
 */
public interface HardDisk 

    // 存储数据
    public void save(String data);

    // 获取数据
    public String get();


以上HardDisk接口定义完毕之后,我们立马定义一个它的子实现类,即希捷硬盘类,该类与咱们上面所写的基本上一模一样,只不过现在它要去实现HardDisk接口了。

package com.meimeixia.principles.demo3.after;

/**
 * 希捷硬盘类
 * @author liayun
 * @create 2021-05-27 19:28
 */
public class XiJieHardDisk implements HardDisk 

    // 存储数据的方法
    public void save(String data) 
        System.out.println("使用希捷硬盘存储数据为:" + data);
    

    // 获取数据的方法
    public String get() 
        System.out.println("使用希捷硬盘获取数据");
        return "数据";
    


接着,定义第二个接口,即Cpu接口。

package com.meimeixia.principles.demo3.after;

/**
 * Cpu接口
 * @author liayun
 * @create 2021-05-27 20:15
 */
public interface Cpu 

    // 运行cpu
    public void run();


以上Cpu接口定义完毕之后,我们立马定义一个它的子实现类,即英特尔cpu类,该类与咱们上面所写的基本上一模一样,只不过现在它要去实现Cpu接口了。

package com.meimeixia.principles.demo3.after;

/**
 * Intel cpu
 * @author liayun
 * @create 2021-05-27 19:33
 */
public class IntelCpu implements Cpu 

    public void run() 
        System.out.println("使用Intel处理器");
    


紧接着,定义第三个接口,即Memory接口。

package com.meimeixia.principles.demo3.after;

/**
 * 内存条接口
 * @author liayun
 * @create 2021-05-27 20:20
 */
public interface Memory 

    public void save();


以上Memory接口定义完毕之后,我们立马定义一个它的子实现类,即KingstonMemory类,该类与咱们上面所写的基本上一模一样,只不过现在它要去实现Memory接口了。

package com.meimeixia.principles.demo3.after;

/**
 * 金士顿内存条类
 * @author liayun
 * @create 2021-05-27 19:36
 */
public class KingstonMemory implements Memory 

    public void save() 
        System.out.println("使用金士顿内存条");
    


以上这些接口以及其子实现类全部定义完毕之后,接下来我们开始定义Computer类。在该类里面,我们要声明以上那些配件,不过,我们现在不能在这一块去声明具体的实现类了,而是应该声明抽象的父接口,就像依赖倒转原则所描述的那样——应该依赖抽象,而不应该依赖细节。

package com.meimeixia.principles.demo3.after;

/**
 * @author liayun
 * @create 2021-05-27 20:36
 */
public class Computer 

    private HardDisk hardDisk;
    private Cpu cpu;
    private Memory memory;

    public HardDisk getHardDisk() 
        return hardDisk;
    

    public void setHardDisk(HardDisk hardDisk) 
        this.hardDisk = hardDisk;
    

    public Cpu getCpu() 
        return cpu;
    

    public void setCpu(Cpu cpu) 
        this.cpu = cpu;
    

    public Memory getMemory() 
        return memory;
    

    public void setMemory(Memory memory) 
        this.memory = memory;
    

    public void run() 
        System.out.println("运行计算机");
        String data = hardDisk.get();
        System.out.println("从硬盘上获取的数据是:" + data);
        cpu.run();
        memory.save();
    


最后,我们还得提供一个测试类用于测试。

package com.meimeixia.principles.demo3.after;

/**
 * @author liayun
 * @create 2021-05-27 20:46
 */
public class ComputerDemo 

    public static void main(String[] args) 
        // 创建计算机的配件对象
        HardDisk hardDisk = new XiJieHardDisk();
        Cpu cpu = new IntelCpu();
        Memory memory = new KingstonMemory();

        // 创建计算机对象
        Computer c = new Computer();
        // 组装计算机
        c.setCpu(cpu);
        c.setHardDisk(hardDisk);
        c.setMemory(memory);

        // 运行计算机
        c.run();
    


这时,不妨来运行一下以上测试类,看一下效果是不是我们所想要的,如下图所示,确实是打印出了我们想要的结果。那么这样的话,以上这个案例就已经改进了。

当然了,如果后期你想要去换一个AMD品牌的cpu的话,那么只需要去新建一个AMD品牌的cpu类,然后让它去实现Cpu接口就可以了,此时,Computer类是不需要进行修改的。接着,你只需要在测试类中创建一个AMD品牌的cpu对象,并将其作为参数进行一个传递,组装到计算机里面即可。这样,就很好地符合了开闭原则。

最后,我做一个总结,面向对象的开发很好的解决了以上案例所存在的问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的耦合度。

通过以上案例的改进,相信大家能看懂以上这句话,要是还不懂,你自个去揣摩吧!

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

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

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

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

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

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

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