设计模式原则之依赖倒转(倒置)原则
原则概述
依赖倒转原则(Dependence Inversion Principle)是指: 高层模块不应该依赖底层模块,二者都应该依赖其(或接口)抽象(不要依赖具体子类) 。也就是说 抽象不应该依赖于细节,细节应当依赖于抽象;其核心思想是要面向接口编程,而不是面向实现编程。
实现方法
具体来说,当我们在代码中传递参数或搭建关联关系时,尽量引用层次高的抽象层类,也就是尽量使用接口和抽象类代替具体类来进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,尽量不要用具体类来做这些事情。
为了确保该原则的有效使用,就要遵守接口隔离原则,即一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法;否则将无法调用在子类中增加的新方法。
引入抽象层后,系统将具有很好的灵活性,在程序中尽量使用抽象层进行编程,而将具体类写在配置文件中,这样一来,如果系统行为发生变化,只需要对抽象层进行扩展,并修改配置文件,而无须修改原有系统的源代码,在不修改的情况下来扩展系统的功能,满足开闭原则的要求。
实现了依赖倒转原则,我们更多的时候是针对抽象层编程,而将具体类的对象通过依赖注入(DependencyInjection, DI)的方式注入到其对应的抽象层中;
依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。
常用的注入方式有三种,分别是:构造注入,设值注入(Setter注入)和接口注入。构造注入是指通过构造函数来传入具体类的对象,设值注入是指通过Setter方法来传入具体类的对象,而接口注入是指通过在接口中声明的业务方法来传入具体类的对象。这些方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象。
小结
依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则:
1、每个类尽量提供接口或抽象类,或者两者都具备。
2、变量的声明类型尽量是接口或者是抽象类。
3、任何类都不应该从具体类派生。
4、使用继承时尽量遵循里氏替换原则
应用案例
比如有个Person类,可以接受Email、QQ和微信的消息。如果都为其提供一个专门的方法,就会让代码非常的冗余:
可以引入一个IReceiver接口,让Person类依赖该接口。这样QQ类、微信类和Email类各自实现IReceiver里面的方法即可:
对应的代码如下,
编程完成Person 接收消息 的功能。
public class DependecyInversion {
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
}
}
class Email {
public String getInfo() {
return "电子邮件信息: hello,world";
}
}
//完成Person接收消息的功能
//方式1分析
//1. 实现简单,比较容易想到
//2. Person类的receive方法直接接收了一个Email,问题是如果我们获取的对象是除了Email,
//还有从 微信,短信中获取信息 等等,则需要新增类,同时Perons也要增加相应的接收方法
//3. 解决思路:引入一个抽象的接口IReceiver, 表示接收者, 这样Person类与接口IReceiver发生依赖
// 因为Email, WeiXin 等等属于接收的范围,他们各自实现IReceiver 接口就ok, 这样我们就符号依赖倒转原则
class Person {
public void receive(Email email ) {
System.out.println(email.getInfo());
}
}
Person类的receive方法直接接收了一个Email,问题是如果我们获取的对象是除了Email,还想要从 微信,短信等对象中获取信息 等等,这样我们就需要新增类,同时Perons也要增加相应的接收方法。
解决方法:引入一个抽象的接口IReceiver, 表示接收者, 这样Person类与接口IReceiver发生依赖, 因为Email, WeiXin 等等属于接收的范围,他们各自实现IReceiver 接口就可以了, 这样我们就符合依赖倒转原则,而Person类不需要新增相应的接收方法,只需要将Person类的receive方法的参数变成IReceiver接口即可。
public class DependecyInversion {
public static void main(String[] args) {
//客户端无需改变
Person person = new Person();
person.receive(new Email());
person.receive(new WeiXin());
}
}
//定义接口
interface IReceiver {
public String getInfo(); //做成抽象方法
}
class Email implements IReceiver {
public String getInfo() {
return "电子邮件信息: hello,world";
}
}
//增加微信,实现IReceiver
class WeiXin implements IReceiver {
public String getInfo() {
return "微信信息: hello,ok";
}
}
//方式2
class Person {
//这里我们是对接口的依赖
public void receive(IReceiver receiver ) {
System.out.println(receiver.getInfo());
}
}
依赖关系传递的三种方式和应用案例
- 接口传递
// 方式1: 通过接口传递实现依赖
// 开关的接口
interface IOpenAndClose {
public void open(ITV tv); // 抽象方法,接收接口
}
interface ITV { // ITV接口
public void play();
}
class ChangHong implements ITV {
@Override
public void play() {
// TODO Auto-generated method stub
System.out.println("长虹电视机,打开");
}
}
实现接口
class OpenAndClose implements IOpenAndClose {
public void open(ITV tv) {
tv.play();
}
}
- 构造方法传递
//方式2:通过构造方法依赖传递
interface IOpenAndClose {
public void open(); // 抽象方法
}
interface ITV { // ITV接口
public void play();
}
class OpenAndClose implements IOpenAndClose {
public ITV tv; // 成员
public OpenAndClose(ITV tv) { // 构造器
this.tv = tv;
}
public void open() {
this.tv.play();
}
}
- setter方式传递
// 方式3 , 通过setter方法传递
interface IOpenAndClose {
public void open(); // 抽象方法
public void setTv(ITV tv);
}
interface ITV { // ITV接口
public void play();
}
class OpenAndClose implements IOpenAndClose {
private ITV tv;
public void setTv(ITV tv) {
this.tv = tv;
}
public void open() {
this.tv.play();
}
}
class ChangHong implements ITV {
@Override
public void play() {
// TODO Auto-generated method stub
System.out.println("长虹电视机,打开");
}
小结:
-
高层模块不应该依赖底层模块,二者都应该依赖其(或接口)抽象(不要依赖具体子类)
-
抽象不应该依赖细节,细节应该依赖抽象
-
依赖倒转(倒置)的中心思想是面向接口编程
-
依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在java中,抽象 指的是接口或抽象类,细节就是具体的实现类
-
遵循依赖倒转的话我们在继承时遵循里氏替换原则( 里氏替换原则规定了在任意父类可以出现的地方,子类都一定可以出现 )