一、基本介绍
依赖倒转原则(Dependence Inversion Principle)是指:
(1)高层模块不应该依赖低层模块,二者都应该依赖其抽象
(2)抽象不应该依赖细节,细节应该依赖抽象
(3)依赖倒转(倒置)的中心思想是面向接口编程
(4)依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在Java中,抽象指的是接口(interface)或抽象类(abstract class),细节就是具体的实现类
(5)使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给它们的实现类去完成
二、应用实例
编程完成Person接收消息功能
具体代码如下:
1 public class DependencyInversion { 2 public static void main(String[] args) { 3 Person person = new Person(); 4 person.receive(new Email()); 5 } 6 } 7 8 class Email { 9 public String getInfo() { 10 return "电子邮件信息:Hello World!"; 11 } 12 } 13 14 //完成Person接收消息 15 class Person { 16 public void receive(Email email) { 17 System.out.println(email.getInfo()); 18 } 19 }
运行结果:
分析:
以上代码虽然完成了功能,但是Person类和Email类之间存在严重的耦合,如果我们接收信息的对象不是Email,而是微信,短信,则需要增加新的类,而且Person类中需要增加新的方法
解决方案:
引入一个接口IReceiver,表示接收者,这样Person类和IReceiver接口发生依赖,因为微信,短信等都属于接收的方式,所以实现IReceiver接口即可,这样就符合了依赖倒转原则
具体代码实现:
1 public class DependencyInversion { 2 public static void main(String[] args) { 3 Person person = new Person(); 4 person.receive(new Email()); 5 person.receive(new Wechat()); 6 } 7 } 8 9 //定义接口 10 interface IReceiver { 11 String getInfo(); 12 } 13 14 //电子邮件 15 class Email implements IReceiver { 16 @Override 17 public String getInfo() { 18 return "电子邮件信息:Hello World!"; 19 } 20 } 21 22 //微信 23 class Wechat implements IReceiver { 24 @Override 25 public String getInfo() { 26 return "微信信息:Hello World!"; 27 } 28 } 29 30 //完成Person接收消息 31 class Person { 32 public void receive(IReceiver iReceiver) { 33 System.out.println(iReceiver.getInfo()); 34 } 35 }
运行结果:
这样写之后,每次如果新增接收消息的方式,直接实现IReceiver接口即可,同时将Person类中的receive方法的参数改为IReceiver接口,也避免了类与类之间的耦合
三、依赖关系传递的三种方式和应用案例
三种方式:接口传递、构造方法传递、setter方式传递
应用实例:编程实现打开电视机操作(重在理解思想)
方式一:
(1)接口传递
1 /** 2 * 通过接口传递实现依赖 3 */ 4 public class InterfaceImplements { 5 public static void main(String[] args) { 6 ChangHongTV changHongTV = new ChangHongTV(); 7 OpenAndClose openAndClose = new OpenAndClose(); 8 openAndClose.open(changHongTV); 9 } 10 } 11 12 //开关的接口 13 interface IOpenAndClose { 14 void open(ITV itv); 15 } 16 17 //TV的接口 18 interface ITV { 19 void play(); 20 } 21 22 //实现接口 23 class OpenAndClose implements IOpenAndClose { 24 @Override 25 public void open(ITV itv) { 26 itv.play(); 27 } 28 } 29 30 //用户自定义电视 31 class ChangHongTV implements ITV { 32 @Override 33 public void play() { 34 System.out.println("长虹电视机已开启!"); 35 } 36 }
运行结果:
分析:首先定义了一个专门用于电视机开关操作的抽象接口IOpenAndClose,然后定义了一个用于描述电视机的抽象接口ITV,接着使用OpenAndClose方法实现IOpenAndClose接口,并且自定义一个电视机类实现ITV接口,在main方法中实例化用户自定义的电视机类并且实例化OpenAndClose类,使用OpenAndClose类中的open方法,将用户自定义的电视机类的对象作为参数传递进去
答疑:
Q:看到这里很多读者可能会疑惑,为什么搞这么麻烦,直接写个开关的类来调用电视机类不就好了吗?
A:这样确实可以,但是代码不够成熟,可拓展性也不好,因为电视不是唯一的,开关也不是唯一的,拟定一个统一的接口就像制定一个标准一样,不管是什么电视什么开关,实现这个接口就必须实现其中的所有抽象方法,实现了这些方法也就遵循了这个标准,有了这两个接口,不管是任何电视任何开关都能适用,降低了各个类之间耦合度的同时也提升了代码的可拓展性
方式二:
(2)构造方法传递
1 /** 2 * 通过构造方法实现依赖传递 3 * */ 4 public class ConstructionImplements { 5 public static void main(String[] args) { 6 OpenAndClose openAndClose = new OpenAndClose(new AppleTV()); 7 openAndClose.open(); 8 } 9 } 10 11 //开关的接口 12 interface IOpenAndClose{ 13 void open(); 14 } 15 16 //TV的接口 17 interface ITV{ 18 void play(); 19 } 20 21 //实现接口 22 class OpenAndClose implements IOpenAndClose{ 23 private ITV itv; 24 25 public OpenAndClose(ITV itv) { 26 this.itv = itv; 27 } 28 29 @Override 30 public void open() { 31 this.itv.play(); 32 } 33 } 34 35 class AppleTV implements ITV{ 36 @Override 37 public void play() { 38 System.out.println("苹果电视已开启!"); 39 } 40 }
运行结果:
分析:开关和电视的接口定义与上面相同不再阐述,不过这里的OpenAndClose类换了另一种方法来调用TV接口,将其作为成员变量,使得OpenAndClose类在实例化的时候就传入TV接口,并在open方法中可以直接调用成员变量的play方法,降低了耦合度的同时提升了效率
方式三:
(3)setter方式传递
1 /** 2 * 通过setter方法实现依赖传递 3 */ 4 public class SetterImplements { 5 public static void main(String[] args) { 6 OpenAndClose openAndClose = new OpenAndClose(); 7 openAndClose.setItv(new XiaomiTV()); 8 openAndClose.open(); 9 } 10 } 11 12 //开关的接口 13 interface IOpenAndClose { 14 void open(); 15 16 void setItv(ITV itv); 17 } 18 19 //TV的接口 20 interface ITV { 21 void play(); 22 } 23 24 //实现接口 25 class OpenAndClose implements IOpenAndClose { 26 private ITV itv; 27 28 @Override 29 public void setItv(ITV itv) { 30 this.itv = itv; 31 } 32 33 @Override 34 public void open() { 35 this.itv.play(); 36 } 37 } 38 39 class XiaomiTV implements ITV { 40 @Override 41 public void play() { 42 System.out.println("小米电视已开启!"); 43 } 44 }
运行结果:
分析:电视的接口定义与上面相同不再阐述,开关接口的定义与上面不同在于,定义了setItv方法,并将TV的接口作为参数,这就意味着实现IOpenAndClose接口必须实现这两个方法,先传入一个TV接口,然后再调用通过该实现该TV接口的类调用open方法
四、注意事项和细节
(1)低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好。
(2)变量的声明类型尽量是抽象类或接口, 这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化。
(3)继承时遵循里氏替换原则