附链
你也可以在这些平台阅读本文:
定义
高层模块不应该依赖底层模块,二者都应该依赖其抽象。
抽象不应该依赖细节,细节应该依赖抽象。
针对接口编程,而不要针对实现编程。
场景示例
假设笔者现在要去超市购物,需要买点可乐和薯片。这一过程笔者分别用面向实现和面向过程两种方式进行实现。
面向实现编程
创建实体类
创建一个人物实体类,同时在人物实体类内部实现购买可乐和购买薯片的两个方法。
/**
* @author zhh
* @description 人物类
* @date 2020-02-05 20:29
*/
public class Person {
/**
* 购买可乐
*/
public void buyCoke() {
System.out.println("笔者买了可乐");
}
/**
* 购买薯片
*/
public void buyCrisps() {
System.out.println("笔者买了薯片");
}
}
测试类及输出
/**
* @author zhh
* @description 测试类
* @date 2020-02-05 20:40
*/
public class Test {
public static void main(String[] args) {
Person person = new Person();
person.buyCoke();
person.buyCrisps();
}
}
测试类的输出结果如下:
笔者买了可乐
笔者买了薯片
存在的问题
假设笔者现在还需购买巧克力,我们如何去实现这个功能?
/**
* @author zhh
* @description 人物类
* @date 2020-02-05 20:29
*/
public class Person {
// 此处省略其余方法
// 新增 购买巧克力 方法
public void buyChocolate() {
System.out.println("笔者买了巧克力");
}
}
我们可以看到这里的人物类是一个实现类,我们会发现这个实现类在功能需求变动时是经常需要进行修改的,类的扩展性比较差。
由于没有进行抽象,造成我们应用层的函数(上述例子当中指 Test 类)修改是依赖于底层实现(上述例子中指 Person 类中的具体方法)。而根据依赖倒置的原则,高层次的模块(Test类)是不应该依赖于低层次的模块(Person类)。
面向接口编程
在这里我们引入抽象,来解决上述面向实现编程所存在的问题。
创建商品接口
创建一个商品接口,同时该接口仅提供一个购买商品的抽象方法,而具体购买哪种商品则交由高层模块进行选择。
/**
* @author zhh
* @description 商品接口
* @date 2020-02-05 20:50
*/
public interface IGoods {
/**
* 购买商品
*/
void buyGood();
}
创建商品实现类
/**
* @author zhh
* @description 可乐商品
* @date 2020-02-05 20:52
*/
public class Coke implements IGoods {
public void buyGood() {
System.out.println("笔者买了可乐");
}
}
/**
* @author zhh
* @description 薯片商品
* @date 2020-02-05 20:52
*/
public class Crisps implements IGoods {
public void buyGood() {
System.out.println("笔者买了薯片");
}
}
/**
* @author zhh
* @description 巧克力商品
* @date 2020-02-05 20:53
*/
public class Chocolate implements IGoods {
public void buyGood() {
System.out.println("笔者买了巧克力");
}
}
调整实现类
将面向实现编程中的 Person 类进行如下调整
/**
* @author zhh
* @description 人物类
* @date 2020-02-05 20:29
*/
public class Person {
public void buyGood(IGoods iGoods) {
iGoods.buyGood();
}
}
这里通过这个方法传入一个对象,而这个对象是需要用接口的,因为具体传入的对象是可乐、薯片还是巧克力是需要依据高层模块来选择的。
测试类
/**
* @author zhh
* @description 测试类
* @date 2020-02-05 20:40
*/
public class Test {
public static void main(String[] args) {
Person person = new Person();
person.buyGood(new Coke());
person.buyGood(new Crisps());
}
}
类结构图
以上示例类的结构图如下所示
优势说明
如果再有其他商品接口的实现,只需要和上述可乐、薯片等具体的商品实现类保持一致,平级扩展即可。
而具体的人物实现类是并不需要改动的。也就是说我们要面向接口编程,所写的扩展类是面向接口的而不是面向具体实现类的。
而对于具体购买什么商品是交给高层模块 Test 类来选择的。
这样的话就做到了人物类和 Test 类之间的解耦,同时人物类和具体的商品实现类又是解耦的。
其他实现
上述面向接口编程的例子是通过接口方法的方式来注入具体的实现,我们也可以采用其他方式来实现。
构造器注入方式
- 调整 Person 人物实现类
public class Person {
private IGoods iGoods;
public Person(IGoods iGoods) {
this.iGoods = iGoods;
}
public void buyGood() {
iGoods.buyGood();
}
}
- 调整测试类
public class Test {
public static void main(String[] args) {
Person person = new Person(new Coke());
person.buyGood();
}
}
setter方法注入方式
- 调整 Person 人物实现类
public class Person {
private IGoods iGoods;
public void setIGoods(IGoods iGoods) {
this.iGoods = iGoods;
}
public void buyGood() {
iGoods.buyGood();
}
}
- 调整测试类
public class Test {
public static void main(String[] args) {
Person person = new Person();
person.setIGoods(new Coke());
person.buyGood();
}
}
优点
- 可以减少类之间的耦合性
- 提高系统的稳定性
- 提高代码的可读性和可维护性
- 降低修改程序所造成的风险
参考
- 《Head First 设计模式》
- 《大话设计模式》