设计模式-依赖倒置原则
在商品经济萌芽的时候出现以物易物,假如买一件衣服,老板要你拿一头猪换,可是你定不会养猪,你只会编程。你找到养猪户,决定写一个APP换他一头猪,他说换猪可以,但是得用一条金链来换...
“所以这里就出现了一连串的对象依赖,从而造成了严重的耦合灾难。解决这个问题的最好的办法就是,买卖双方都依赖于抽象——也就是货币——来进行交换,这样一来耦合度就大为降低了。”
1. 概述
依赖倒置原则的原始定义为:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。其核心思想是:要面向接口编程,不要面向实现编程。依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。
1.1. 依赖倒置原则的作用
- 依赖倒置原则可以降低类间的耦合性;
- 依赖倒置原则可以提高系统的稳定性;
- 依赖倒置原则可以减少并行开发引起的风险;
- 依赖倒置原则可以提高代码的可读性和可维护性;
1.2. 程序举例
我们来实现小明踢足球的业务
package com.skystep.design.undip;
public class Player {
private Football football = new Football();
public void play() {
football.play();
}
}
package com.skystep.design.undip;
public class Football {
public void play() {
System.out.println("play football");
}
}
package com.skystep.design.undip;
public class Client {
public static void main(String[] args) {
Player xiaoming = new Player();
xiaoming.play();
}
}
看似我们已经完成小明踢足球的业务,并且大多数初级 JAVA 程序员会这么写。但是业务是多变的,有一天产品和我们说小明要开始打篮球了,好吧,现在显然是无法满足的,这怎么搞,增加一个 Basketball 类,修改 Player 类?那后续要改成高尔夫球呢,反反复复。在此代码中 Player 类还比较简单,如果非常复杂,如何解决。尝试换种思路吧。
package com.skystep.design.dip;
public interface Ball {
void play();
}
public class Basketball implements Ball {
public void play() {
System.out.println("play basketball");
}
}
public class Football implements Ball {
public void play() {
System.out.println("play football");
}
}
package com.skystep.design.dip;
public class Player {
private Ball ball;
public Player(Ball ball) {
this.ball = ball;
}
public void play() {
ball.play();
}
}
package com.skystep.design.dip;
public class Client {
public static void main(String[] args) {
// 踢足球
Football football = new Football();
Player xiaoming = new Player(football);
// 打篮球
// Basketball basketball = new Basketball();
// Player xiaoming = new Player(basketball);
}
}
看似我们已经完成小明踢足球和打篮球的业务,后续产品和我们说小明要打高尔夫了,我们只需增加高尔夫球类,在客户端类中增加业务代码即可,这样一来,Player 这个核心业务类便无需变动。这样的代码更加健壮和更容易扩展,更加符合业务的多变。
1.3. 总结
从代码来看,Player 类是依赖类,属于高层模块,Ball 类是被依赖类,属于低层模块,只不过我们把他抽象(接口或者抽象类)了,这样做 Player 的依赖是抽象的,细节放到具体类中实现,只要调用者自行决定使用哪一个具体类(多态)。
从依赖上来讲,这和我们的常规思维有些不同,一个运动员修改了他的体育爱好,则应该修改爱好类,然后修改运动员类,这样逐级往上修改,但是这样的代码结构调整在浩大的工程面前变得十分吃力。显然遵守依赖倒置原则可以有效解决这个问题。
但事实上,这可能不是很好理解,如果好理解便不会那么多 JAVA 程序员写出最开始的小明踢足球的业务代码。因此在实际编程时,提倡面向接口编程,不要面向实现编程。
2. 实现方法
2.1. 遵循规则
依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则:
- 每个类尽量提供接口或抽象类,或者两者都具备。
- 变量的声明类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 使用继承时尽量遵循里氏替换原则。
2.2. 具体实现
通常将被依赖类抽象化之后,如何注入到依赖类中,推荐由两种做法:
- 构造注入:利用构造方法注入
- setter方法注入:在需要注入的类里提供一个setter方法
package com.skystep.design.dip;
// 构造注入
public class Player {
private Ball ball;
public Player(Ball ball) {
this.ball = ball;
}
public void play() {
ball.play();
}
}
package com.skystep.design.dip;
// setter方法注入
public class Player {
private Ball ball;
public setBall(Ball ball) {
this.ball = ball;
}
public void play() {
ball.play();
}
}
3. 扩展解读
3.1. Spring IOC
在上面的例子中,我们都是通过手动的方式来创建依赖对象,然后在手动传递给被依赖模块(高层),但对于大型的项目来说,各个组件之间的依赖关系式非常复杂的,如果我们还是用手动来创建依赖对象并且手动注入是个相当繁杂的一个工作,而且还容易出错,甚至出现不可控状态。Spring IOC 便可以解决这样的问题。
IoC 不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是 松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。
可以说 Spring IOC 完全遵循了依赖倒置原则,在这基础上,接管了所有对象的管理,不再需要我们自己创建。创建了一套机制实现不同对象之间的依赖注入。开发着不需要关心对象与对象之间的复杂依赖关系。只需一个 xml 配置或者一个注解便可以实现依赖的注入,后续需要变化只需修改接口的实现即可,并不需要对上下层模块最太大的改动。
例如:对于一个 web 开发,原先使用的数据库是 mysql,后续使用 oracle, 我们只需要替换数据访问层对应接口的实现类且保证新类和旧类测试结果相同即可,更高层模块,如服务层,控制层无需任何代码改动,不用重新编写测试代码。可以说在编程思维小小的进步,便能减少工作量,提高质量。
3.2. 实际应用
生活中有很多遵循依赖倒置的原则的设计,例如电脑外设(鼠标、键盘),对于电脑主机来讲,替换鼠标并不需要拆除电脑主机,因为主机对接的鼠标接口,换哪一个鼠标(具体实现),主机定不关心。在设计电脑之前,工程师就已经站在更高模块来设计这些预留接口。主机是高层模块,外设是底层模块,高层模块不依赖底层模块具体实现,而是依赖外设插口(接口抽象)。
此外,ATM机的设计也是如此,ATM上提供了一个卡槽插口(接口),供各种银行卡插入使用,在这里ATM机不依赖于具体的哪种银行卡,它只规定了银行卡的规格,只要我们手上的银行卡满足这个规格参数,我们就可以使用它。