从零开始学习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设计模式 | 软件设计原则篇:合成复用原则