定义:
摘自设计模式之禅
-
High level modules should not depend upon low level modules.
高级模块不应该依赖于低级模块。
-
Both should depend upon abstractions.
两者都应该依赖于抽象。
-
Abstractions should not depend upon details.
抽象不应该依赖于细节。
-
Details should depend upon abstractions.
细节应该依赖于抽象。
依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在java 中,抽象指的是接口或抽象类,细节就是具体的实现类
也就是我们所谓的“面向接口编程OOD(Object-Oriented Design 面向对象设计)” ,依赖倒置原则的核心思想是面向接口编程,
使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,提高代码的可读性和可维护性。
问题由来:
我们依旧用一个例子来说明面向接口编程比相对于面向实现编程好在什么地方。
实现Person 接收消息的功能,可以实现接收Email,Weixin等 。
类Person 直接依赖类Email,假如要将类Person 改为依赖类Weixin,则必须通过修改类Person 的代码来达成。这种场景下,类Person 一般是高层模块,负责复杂的业务逻辑;类Email和类Weixin是低层模块,负责基本的原子操作;假如修改类Person ,会给程序带来不必要的风险。
代码如下:
package com.dongguo.principle.inversioninversion1;
/**
* @author Dongguo
* @date 2021/8/21 0021-22:14
* @description:
*/
public class Email {
public String getInfo() {
return "电子邮件信息: hello,world";
}
}
package com.dongguo.principle.inversioninversion1;
/**
* @author Dongguo
* @date 2021/8/21 0021-22:20
* @description: Person类
*/
public class Person {
/**
* @author Dongguo
* @description: 接收消息的功能
*/
public void receive(Email email) {
System.out.println(email.getInfo());
}
}
package com.dongguo.principle.inversioninversion1;
/**
* @author Dongguo
* @date 2021/8/21 0021-22:14
* @description:
*/
public class DependecyInversion {
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
}
}
运行结果:
电子邮件信息: hello,world
分析
这种方法简单,比较容易想到
问题:如果我们获取的信息是微信,短信等等,则需要新增类,同时Perons 也必须要增加相应的接收方法,原因就是Perons 与Email 之间的耦合性太高了,必须降低他们之间的耦合度才行。
解决方案:引入一个抽象的接口IReceiver, 表示接收功能, 这样Person 类与接口IReceiver 发生依赖
因为Email, WeiXin 等等属于接收的范围,他们各自实现IReceiver 接口就ok了, 这样我们就符和依赖倒转原则
package com.dongguo.principle.inversioninversion2;
/**
* @author Dongguo
* @date 2021/8/21 0021-22:33
* @description:接收信息接口
*/
public interface IReceiver {
public String getInfo();
}
package com.dongguo.principle.inversioninversion2;
/**
* @author Dongguo
* @date 2021/8/21 0021-22:34
* @description: 接收Email信息
*/
public class Email implements IReceiver {
public String getInfo() {
return "电子邮件信息: hello,world";
}
}
package com.dongguo.principle.inversioninversion2;
/**
* @author Dongguo
* @date 2021/8/21 0021-22:33
* @description: 接收weixin信息
*/
public class WeiXin implements IReceiver {
public String getInfo() {
return "微信信息: hello,ok";
}
}
package com.dongguo.principle.inversioninversion2;
/**
* @author Dongguo
* @date 2021/8/21 0021-22:34
* @description:
*/
public class Person {
/**
* @author Dongguo
* @description: 这里我们是对接口的依赖而非具体实现类
*/
public void receive(IReceiver receiver ) {
System.out.println(receiver.getInfo());
}
}
package com.dongguo.principle.inversioninversion2;
/**
* @author Dongguo
* @date 2021/8/21 0021-22:34
* @description:
*/
public class DependecyInversion {
public static void main(String[] args) {
//客户端无需改变
Person person = new Person();
person.receive(new Email());
person.receive(new WeiXin());
}
}
运行结果:
电子邮件信息: hello,world
微信信息: hello,ok
这样修改后,无论以后怎样扩展,都不需要再修改Person 类了,只需要创建一个具体实现类实现IReceiver接口。
实际情况中,代表高层模块的Person 类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。
采用依赖倒置原则给多人并行开发带来了极大的便利,比如上例中,原本Person 类与Email类直接耦合时,Person类必须等Email 类编码完成后才可以进行编码,因为Person 类依赖于Email类。修改后的程序则可以同时开工,互不影响,因为Person 与Email 类一点关系也没有。参与协作开发的人越多、项目越庞大,采用依赖导致原则的意义就越重大。现在很流行的TDD开发模式就是依赖倒置原则最成功的应用。
依赖关系传递的三种方式和应用案例传递依赖关系有三种方式,以上的例子中使用的方法是接口传递,另外还有两种传递方式:构造方法传递和setter方法传递,相信用过Spring框架的,对依赖的传递方式一定不会陌生。
实例:实现打开TV的功能
接口传递
抽象出一个TV的接口和一个开关的接口
package com.dongguo.principle.inversioninversion3;
/**
* @author Dongguo
* @date 2021/8/21 0021-22:48
* @description: TV 接口
*/
public interface ITV {
public void play();
}
package com.dongguo.principle.inversioninversion3;
/**
* @author Dongguo
* @date 2021/8/21 0021-22:49
* @description: TV的实现类
*/
public class ChangHong implements ITV {
@Override
public void play() {
System.out.println("长虹电视机,打开");
}
}
package com.dongguo.principle.inversioninversion3;
/**
* @author Dongguo
* @date 2021/8/21 0021-22:49
* @description: 开关的接口, 通过接口传递实现依赖
*/
public interface IOpenAndClose {
public void open(ITV tv); //抽象方法,接收接口
}
package com.dongguo.principle.inversioninversion3;
/**
* @author Dongguo
* @date 2021/8/21 0021-22:50
* @description: 实现IOpenAndClose接口
*/
public class OpenAndClose implements IOpenAndClose {
public void open(ITV tv) {
tv.play();
}
}
package com.dongguo.principle.inversioninversion3;
/**
* @author Dongguo
* @date 2021/8/21 0021-22:51
* @description:
*/
public class DependencyPass {
public static void main(String[] args) {
ChangHong changHong = new ChangHong();
OpenAndClose openAndClose = new OpenAndClose();
openAndClose.open(changHong);
}
}
运行结果:
长虹电视机,打开
构造方法传递
package com.dongguo.principle.inversioninversion.inversioninversion4;
/**
* @author Dongguo
* @date 2021/8/21 0021-22:56
* @description:
*/
public interface ITV {
public void play();
}
package com.dongguo.principle.inversioninversion.inversioninversion4;
/**
* @author Dongguo
* @date 2021/8/21 0021-23:02
* @description:
*/
public class ChangHong implements ITV {
@Override
public void play() {
System.out.println("长虹电视机,打开");
}
}
package com.dongguo.principle.inversioninversion.inversioninversion4;
/**
* @author Dongguo
* @date 2021/8/21 0021-23:02
* @description:
*/
public interface IOpenAndClose {
public void open(); //抽象方法
}
package com.dongguo.principle.inversioninversion.inversioninversion4;
/**
* @author Dongguo
* @date 2021/8/21 0021-23:03
* @description:
*/
public class OpenAndClose implements IOpenAndClose {
public ITV tv; //成员
public OpenAndClose(ITV tv) { //构造方法
this.tv = tv;
}
public void open() {
this.tv.play();
}
}
package com.dongguo.principle.inversioninversion.inversioninversion4;
/**
* @author Dongguo
* @date 2021/8/21 0021-23:03
* @description:
*/
public class DependencyPass {
public static void main(String[] args) {
ChangHong changHong = new ChangHong();
//通过构造器进行依赖传递
OpenAndClose openAndClose = new OpenAndClose(changHong);
openAndClose.open();
}
}
运行结果:
长虹电视机,打开
setter方法传递
package com.dongguo.principle.inversioninversion.inversioninversion5;
/**
* @author Dongguo
* @date 2021/8/21 0021-23:05
* @description:
*/
public interface ITV {
public void play();
}
package com.dongguo.principle.inversioninversion.inversioninversion5;
/**
* @author Dongguo
* @date 2021/8/21 0021-23:05
* @description:
*/
public class ChangHong implements ITV {
@Override
public void play() {
System.out.println("长虹电视机,打开");
}
}
package com.dongguo.principle.inversioninversion.inversioninversion5;
/**
* @author Dongguo
* @date 2021/8/21 0021-23:06
* @description: 通过setter 方法传递
*/
public interface IOpenAndClose {
public void open(); // 抽象方法
public void setTv(ITV tv);
}
package com.dongguo.principle.inversioninversion.inversioninversion5;
/**
* @author Dongguo
* @date 2021/8/21 0021-23:06
* @description:
*/
public class OpenAndClose implements IOpenAndClose {
private ITV tv;
public void setTv(ITV tv) {
this.tv = tv;
}
public void open() {
this.tv.play();
}
}
package com.dongguo.principle.inversioninversion.inversioninversion5;
/**
* @author Dongguo
* @date 2021/8/21 0021-23:07
* @description:
*/
public class DependencyPass {
public static void main(String[] args) {
ChangHong changHong = new ChangHong();
//通过setter 方法进行依赖传递
OpenAndClose openAndClose = new OpenAndClose();
openAndClose.setTv(changHong);
openAndClose.open();
}
}
运行结果:
长虹电视机,打开
在实际编程中,我们一般需要做到如下:
- 每个类尽量都要有抽象类或接口,或者两者都有。这是依赖倒置的基本要求,接口和抽象类都是属于抽象的,有了抽象才能依赖倒置
- 变量的声明类型尽量是抽象类或接口。(并非是一定要接口或者抽象类,比如工具类xxxUtils一般不需要接口或者抽象类的)
- 使用继承时遵循里氏替换原则。
- 任何类都不应该从具体类派生
- 尽量不要复写基类的方法
依赖倒置原则的核心就是要我们面向接口编程,通过抽象(接口或者抽象类)使各个类或者模块的实现彼此独立,不互相影响,实现模块间的松耦合。
涉及的设计模式有:工厂方法模式、模板方法模式、迭代子模式等。