👉文章示例代码👈

定义

高层模块不应该依赖底层模块,二者都应该依赖其抽象。

抽象不应该依赖细节,细节应该依赖抽象。

针对接口编程,而不要针对实现编程。

场景示例

假设笔者现在要去超市购物,需要买点可乐和薯片。这一过程笔者分别用面向实现和面向过程两种方式进行实现。

面向实现编程

创建实体类

创建一个人物实体类,同时在人物实体类内部实现购买可乐和购买薯片的两个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @author zhh
* @description 人物类
* @date 2020-02-05 20:29
*/
public class Person {

/**
* 购买可乐
*/
public void buyCoke() {
System.out.println("笔者买了可乐");
}

/**
* 购买薯片
*/
public void buyCrisps() {
System.out.println("笔者买了薯片");
}
}

测试类及输出

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @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();
}
}

测试类的输出结果如下:

笔者买了可乐
笔者买了薯片

存在的问题

假设笔者现在还需购买巧克力,我们如何去实现这个功能?

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author zhh
* @description 人物类
* @date 2020-02-05 20:29
*/
public class Person {
// 此处省略其余方法

// 新增 购买巧克力 方法
public void buyChocolate() {
System.out.println("笔者买了巧克力");
}
}

我们可以看到这里的人物类是一个实现类,我们会发现这个实现类在功能需求变动时是经常需要进行修改的,类的扩展性比较差。

由于没有进行抽象,造成我们应用层的函数(上述例子当中指 Test 类)修改是依赖于底层实现(上述例子中指 Person 类中的具体方法)。而根据依赖倒置的原则,高层次的模块(Test类)是不应该依赖于低层次的模块(Person类)。

面向接口编程

在这里我们引入抽象,来解决上述面向实现编程所存在的问题。

创建商品接口

创建一个商品接口,同时该接口仅提供一个购买商品的抽象方法,而具体购买哪种商品则交由高层模块进行选择。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @author zhh
* @description 商品接口
* @date 2020-02-05 20:50
*/
public interface IGoods {

/**
* 购买商品
*/
void buyGood();
}

创建商品实现类

1
2
3
4
5
6
7
8
9
10
11
/**
* @author zhh
* @description 可乐商品
* @date 2020-02-05 20:52
*/
public class Coke implements IGoods {

public void buyGood() {
System.out.println("笔者买了可乐");
}
}
1
2
3
4
5
6
7
8
9
10
11
/**
* @author zhh
* @description 薯片商品
* @date 2020-02-05 20:52
*/
public class Crisps implements IGoods {

public void buyGood() {
System.out.println("笔者买了薯片");
}
}
1
2
3
4
5
6
7
8
9
10
11
/**
* @author zhh
* @description 巧克力商品
* @date 2020-02-05 20:53
*/
public class Chocolate implements IGoods {

public void buyGood() {
System.out.println("笔者买了巧克力");
}
}

调整实现类

将面向实现编程中的 Person 类进行如下调整

1
2
3
4
5
6
7
8
9
10
11
/**
* @author zhh
* @description 人物类
* @date 2020-02-05 20:29
*/
public class Person {

public void buyGood(IGoods iGoods) {
iGoods.buyGood();
}
}

这里通过这个方法传入一个对象,而这个对象是需要用接口的,因为具体传入的对象是可乐、薯片还是巧克力是需要依据高层模块来选择的。

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @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());
}
}

类结构图

以上示例类的结构图如下所示

image.png

优势说明

如果再有其他商品接口的实现,只需要和上述可乐、薯片等具体的商品实现类保持一致,平级扩展即可。

而具体的人物实现类是并不需要改动的。也就是说我们要面向接口编程,所写的扩展类是面向接口的而不是面向具体实现类的。

而对于具体购买什么商品是交给高层模块 Test 类来选择的。

这样的话就做到了人物类和 Test 类之间的解耦,同时人物类和具体的商品实现类又是解耦的。

其他实现

上述面向接口编程的例子是通过接口方法的方式来注入具体的实现,我们也可以采用其他方式来实现。

构造器注入方式

  • 调整 Person 人物实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Person {

    private IGoods iGoods;

    public Person(IGoods iGoods) {
    this.iGoods = iGoods;
    }

    public void buyGood() {
    iGoods.buyGood();
    }
    }
  • 调整测试类

    1
    2
    3
    4
    5
    6
    7
    public class Test {

    public static void main(String[] args) {
    Person person = new Person(new Coke());
    person.buyGood();
    }
    }

setter方法注入方式

  • 调整 Person 人物实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Person {

    private IGoods iGoods;

    public void setIGoods(IGoods iGoods) {
    this.iGoods = iGoods;
    }

    public void buyGood() {
    iGoods.buyGood();
    }
    }
  • 调整测试类

    1
    2
    3
    4
    5
    6
    7
    8
    public class Test {

    public static void main(String[] args) {
    Person person = new Person();
    person.setIGoods(new Coke());
    person.buyGood();
    }
    }

优点

  • 可以减少类之间的耦合性
  • 提高系统的稳定性
  • 提高代码的可读性和可维护性
  • 降低修改程序所造成的风险

参考

  • 《Head First 设计模式》
  • 《大话设计模式》