依赖倒转原则: 面向接口编程,而不是面向实现类
含义
- 依赖倒置原则(
Dependence Inversion Principle
,DIP
)是指设计代码结构时,高层(调用层)模块不应该依赖底层(被调用层)模块,二者都应该依赖其抽象 - 程序依赖接口,接口定义标准和规范,具体的实现按照场景而不是使用对象硬编码的方式实现
- 抽象不应该依赖细节,细节应该依赖抽象。即面向接口编程,不要面向实现编程
- 依赖倒置原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构要比以细节为基础的架构要稳定的多
- 在Java中,抽象指的是接口或者抽象类,细节就是具体的实现类
通俗点说:要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
核心思想
- 面向接口编程,不要面向实现编程
优点
- 减少类与类之间的耦合性,提高系统的稳定性,提高代码的可读性和可维护性,并能够降低修改程序所造成的风险
实现方式
- 传递依赖关系有三种方式:接口传递(方法注入)、构造方法传递、setter方法传递
- 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成
- 低层模板尽量都要有抽象类或接口,或者两个都有,程序稳定性更好
- 变量的声明类型尽量都是抽象类或接口,这样我们的变量变量引用和实际对象间就存在一个缓冲层,有利于程序的扩展和优化
- 继承时遵循里氏替换原则
案例
组装电脑
类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
解决方案:将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。
现在我们要组装一台电脑,那么就得需要一些配件了,例如CPU、硬盘、内存条。只有这些配件都有了,计算机才能正常的运行。选择CPU的话,它又有很多品牌,比如说Intel、AMD等等,硬盘也是一样,它也有很多品牌,如希捷、西数等等,内存条同样也是,有金士顿、海盗船等等这些品牌。所以,我们在组装电脑的时候,可以选择任意一个品牌的配件来进行组装。
从以上类图中可以看到,有一个希捷硬盘类,它里面既有存储数据的方法,也有获取数据的方法;其次,还有一个英特尔CPU类,它里面有一个运行的方法,即run()方法;最后还有一个金士顿内存条类,它里面有保存数据的方法。
注意,以上三个类均不能独立运行,而是必须组合到Computer类里面,所以,你会看到在Computer类里面是以成员变量的形式来声明希捷硬盘、英特尔CPU、金士顿内存条等等这些配件的,继而我们就得为这些成员变量提供相应的getter和setter方法了。此外,在Computer类里面还定义了一个run()方法,该方法就是用来运行计算机的,当然了,计算机的运行肯定是包含了希捷硬盘、英特尔CPU、金士顿内存条等等这些配件的运行的。
接下来,我们便来通过代码实现以上组装电脑案例。
代码
public class Computer {
private InterCpu interCpu;
private KingstonMemory kingstonMemory;
private XiJieHardDisk xiJieHardDisk;
public InterCpu getInterCpu() {
return interCpu;
}
public void setInterCpu(InterCpu interCpu) {
this.interCpu = interCpu;
}
public KingstonMemory getKingstonMemory() {
return kingstonMemory;
}
public void setKingstonMemory(KingstonMemory kingstonMemory) {
this.kingstonMemory = kingstonMemory;
}
public XiJieHardDisk getXiJieHardDisk() {
return xiJieHardDisk;
}
public void setXiJieHardDisk(XiJieHardDisk xiJieHardDisk) {
this.xiJieHardDisk = xiJieHardDisk;
}
public void run() {
System.out.println("运行计算机");
// 计算机运行时,各个配件应各司其职,干自己的活
String data = xiJieHardDisk.get();
System.out.println("从硬盘上获取的数据是:" + data);
interCpu.run();
KingstonMemory.save();
}
}
public class InterCpu {
public void run(){
System.out.println("使用Intel处理器");
}
}
public class KingstonMemory {
public static void save() {
System.out.println("使用金士顿内存条");
}
}
public class XiJieHardDisk {
public void save(String data){
System.out.println("使用希捷硬盘保存数据:"+data);
}
public String get(){
System.out.println("获取希捷硬盘中的数据");
return "希捷硬盘中的数据";
}
}
Main
public static void main(String[] args) {
// 创建配件
InterCpu interCpu = new InterCpu();
KingstonMemory kingstonMemory = new KingstonMemory();
XiJieHardDisk xiJieHardDisk = new XiJieHardDisk();
// 创建算计
Computer computer = new Computer();
// 组装计算机
computer.setInterCpu(interCpu);
computer.setKingstonMemory(kingstonMemory);
computer.setXiJieHardDisk(xiJieHardDisk);
// 运行
computer.run();
}
从以上代码中可以看到,已经组装了一台计算机,但是似乎组装的计算机的CPU只能是Intel的,内存条只能是金士顿的,硬盘只能是希捷的,因为在Computer类里面成员变量声明的是固定品牌的配件,如果我们后期想换成其他品牌的配件的话,那么还得需要去修改Computer类,这就违背开闭原则了。
因此,这对用户肯定是不友好的,用户有了机箱之后,肯定是想按照自己的喜好选择自己喜欢的配件进行组装。
案例改进
给每个配件添加一个接口,不同品牌去实现这个接口
代码
public class Computer {
private Cpu cpu;
private Memory memory;
private 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 HardDisk getHardDisk() {
return hardDisk;
}
public void setHardDisk(HardDisk hardDisk) {
this.hardDisk = hardDisk;
}
public void run() {
System.out.println("运行计算机");
// 计算机运行时,各个配件应各司其职,干自己的活
String data = hardDisk.get();
System.out.println("从硬盘上获取的数据是:" + data);
cpu.run();
memory.save();
}
}
public interface Cpu {
public void run();
}
public interface HardDisk {
public void save(String data);
public String get();
}
public interface Memory {
public void save();
}
public class InterCpu implements Cpu {
@Override
public void run(){
System.out.println("使用Intel处理器");
}
}
public class XiJieHardDisk implements HardDisk{
@Override
public void save(String data){
System.out.println("使用希捷硬盘保存数据:"+data);
}
@Override
public String get(){
System.out.println("获取希捷硬盘中的数据");
return "希捷硬盘中的数据";
}
}
public class KingstonMemory implements Memory{
@Override
public void save() {
System.out.println("使用金士顿内存条");
}
}
Main
public static void main(String[] args) {
// 创建配件
KingstonMemory kingstonMemory = new KingstonMemory();
XiJieHardDisk xiJieHardDisk = new XiJieHardDisk();
// 任意选择CPU
AMDCpu amdCpu = new AMDCpu();
InterCpu interCpu = new InterCpu();
// 创建算计
Computer computer = new Computer();
// 组装计算机
//computer.setCpu(interCpu);
computer.setCpu(amdCpu);
computer.setMemory(kingstonMemory);
computer.setHardDisk(xiJieHardDisk);
// 运行
computer.run();
}
当然了,如果后期你想要去换一个AMD品牌的CPU的话,那么只需要去新建一个AMD品牌的CPU类,然后让它去实现CPU接口就可以了,此时,Computer类是不需要进行修改的。接着,你只需要在测试类中创建一个AMD品牌的CPU对象,并将其作为参数进行一个传递,组装到计算机里面即可。这样,就很好地符合了开闭原则。
最后,做一个总结,面向对象的开发很好的解决了以上案例所存在的问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的耦合度。