👉文章示例代码👈

定义

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

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

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

场景示例

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

面向实现编程

创建实体类

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

/**
 * @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 设计模式》
  • 《大话设计模式》