我的另一篇文章 :《以面向对象的角度分析抽象类和接口》
依赖倒置
依赖倒置原则(Dependence Inversion Principle,DIP)是指设计代码结构时,高层模块不应该依赖低层模块,二者都应该依赖其抽象。
通过依赖倒置,可以减少类与类之间的耦合性,提高系统的稳定性,提高代码的可读性和可维护性,并且能够降低修改程序所造成的风险。
比如,我们有一个TeaRoom类(茶室),有drinkBlackTea 喝红茶与drinkGreenTea 喝绿茶两个方法,来模拟去茶室喝茶
代码如下(完整例子放在github上了)
public class TeaRoom {
public void drinkBlackTea() {
System.out.println("喝"+new BlackTea().aboutTea());
}
public void drinkGreenTea() {
System.out.println("喝"+new GreenTea().aboutTea());
}
}
public class BlackTea {
public String aboutTea() {
return "红茶";
}
}
public class ExampleUnitTest {
@Test
public void main() {
/**
* 常规实现,茶馆与茶叶耦合度太高,只能调用TeaRoom中提供的drinkBlackTea和drinkGreenTea
* 要是想点其它茶叶,需要修改TeaRoom的代码
*/
TeaRoom teaRoom = new TeaRoom();
teaRoom.drinkBlackTea();
teaRoom.drinkGreenTea();
}
}
常规实现,茶馆与茶叶耦合度太高,只能调用TeaRoom中提供的drinkBlackTea和drinkGreenTea
要是想点其它茶叶,需要修改TeaRoom的代码。
利用依赖倒置原则修改一下
public interface ITea {
String aboutTea();
}
/**
* 使用依赖倒置原则解耦
*/
public class TeaRoom2 {
private final IFeel mFeel;
public TeaRoom2(IFeel feel) {
this.mFeel = feel;
}
public void drinkTea(ITea iTea) { //依赖注入
String tea = iTea.aboutTea();
System.out.println("喝了" + tea);
mFeel.feel(tea + "非常好喝");
}
interface IFeel {
void feel(String feel);
}
}
public class ExampleUnitTest {
@Test
public void main() {
/**
* 常规实现,茶馆与茶叶耦合度太高,只能调用TeaRoom中提供的drinkBlackTea和drinkGreenTea
* 要是想点其它茶叶,需要修改TeaRoom的代码
*/
TeaRoom teaRoom = new TeaRoom();
teaRoom.drinkBlackTea();
teaRoom.drinkGreenTea();
/**
* 依赖倒置解耦
*
* 高层模块不依赖底层模块,他们都应该依赖抽象
* 解释:这里相当于高层模块,底层模块是TeaRoom(茶馆),我们去茶馆喝茶,应该是我们点什么茶上什么茶(茶是抽象,只能是茶不能点其它不是茶的),
* 不依赖茶馆的具体实现,茶馆就是提供喝茶的,具体上什么茶取决于点什么茶,只要点的是茶(抽象),就行
* 接口 ITea 是茶的抽象,高层和底层都是依赖该抽象的
*/
TeaRoom2 teaRoom2 = new TeaRoom2(new TeaRoom2.IFeel() {
@Override
public void feel(String feel) {
/**
* 控制反转
*
* 比方说去茶馆喝茶,这个主动权是在调用方,也就是我们这里
*
* 而茶叶好不好喝跟泡茶的方式或者手法是有很大关系的(只考虑客观因素),喝茶的感受跟茶馆有很大的关系
* 也就是说感受好与坏,主动权在茶馆,
*
* 主动权在被调用方就是控制反转,体现在程序上就是 发布/订阅
*/
System.out.println(feel);
}
});
/**
* 依赖注入
*
* 通过注入的方式获得依赖
* A对象依赖于B对象,等价于A对象内部存在对B对象的“调用”,而前提是A对象内部拿到了B对象的引用
* B对象的引用的来源无非有以下几种:A对象内部创建(无论是作为字段还是作为临时变量)、构造器注入、属性注入、方法注入
*/
teaRoom2.drinkTea(new BlackTea());
teaRoom2.drinkTea(new JasmineTea());
teaRoom2.drinkTea(new GreenTea());
}
}
运行结果
控制反转
当存在依赖倒置的时候往往也存在着控制反转
控制反转跟依赖倒置都是一种编程思想,依赖倒置着眼于调用的形式,而控制反转则着眼于程序流程的控制权
比方说去茶馆喝茶,这个主动权是在调用方,也就是我们这里
而茶叶好不好喝跟泡茶的方式或者手法是有很大关系的(只考虑客观因素),喝茶的感受跟茶馆有很大的关系
也就是说感受好与坏,主动权在茶馆,
主动权在被调用方就是控制反转,体现在程序上就是 发布/订阅,就如例子中的TeaRoom2.IFeel()回调
依赖注入
通过注入的方式获得依赖
A对象依赖于B对象,等价于A对象内部存在对B对象的“调用”,而前提是A对象内部拿到了B对象的引用
B对象的引用的来源无非有以下几种:A对象内部创建(无论是作为字段还是作为临时变量)、构造器注入、属性注入、方法注入
就如例子中的teaRoom2.drinkTea(new BlackTea());
面向接口编程
无论是依赖倒置、控制反转、还是依赖注入,都已经蕴含着“面向接口编程”的思想。面向接口,就意味着面向抽象。作为哲学范畴而言,规定性少称为抽象,规定性多称为具体。而接口,就是程序中的一种典型的“抽象”的形式。
我们对理想的编程通常概括为“高内聚,低耦合”,
内聚指的是专人专职,专门的类做专门的事;
在面向对象编程中,对象自身是内聚的,是保管好自己的数据,完成好自己的操作的,而对外界呈现出自己的状态和行为。一个对象往往不能干所有的事,都要与其它对象产生联系,一个对象和另一个对象产生依赖关系,也就是耦合。
面向对象编程其实更偏重于高内聚,面向接口更体现在低耦合
面向接口编程,目的是解耦,保证编程的健壮性、灵活性和可维护性