定义

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

定义补充

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

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

示例

场景

假设 Hilox 现在要去超市购物,需要买点可乐和薯片

面向实现编程

创建实体类

创建 Hilox 人物类,同时实现购买可乐、购买薯片两个方法。

/**
 * @author zhh
 * @description 人物类
 * @date 2019-07-29 17:06
 */
public class Hilox {

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

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

测试类及输出

/**
 * @author zhh
 * @description 测试类
 * @date 2019-07-29 16:41
 */
public class Test {

    public static void main(String[] args) {
        Hilox hilox = new Hilox();
        hilox.buyCoke();
        hilox.buyCrisps();
    }
}

Hilox买了点可乐

Hilox买了点薯片

存在问题

假设 Hilox 现在还要购买巧克力,那么需要在上述 Hilox 类中补充实现。

/**
 * @author zhh
 * @description 人物类
 * @date 2019-07-29 17:06
 */
public class Hilox {
    ...

    /**
     * 购买巧克力
     */
    public void buyChocolate() {
        System.out.println("Hilox买了点巧克力");
    }
}

可以看出这里的 Hilox 类就是一个实现类,我们会发现这个类是经常需要修改的,扩展性比较差。

由于没有抽象,造成我们应用层的函数(上述例子中指 Test 类)修改是依赖于底层实现(上述例子中指 Hilox 类中的具体方法)的。

根据依赖倒置的原则,高层次的模块(Test类)是不应该依赖于低层次的模块(Hilox类)。

面向接口编程

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

创建商品接口

这里仅提供一个购买商品的抽象方法,具体购买哪种商品交由高层模块选择。

/**
 * @author zhh
 * @description 商品接口
 * @date 2019-07-29 17:48
 */
public interface IGoods {

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

创建商品实现类

/**
 * @author zhh
 * @description 可乐
 * @date 2019-07-29 17:54
 */
public class Coke implements IGoods {

    public void buyGood() {
        System.out.println("Hilox买了点可乐");
    }
}
/**
 * @author zhh
 * @description 薯片
 * @date 2019-07-29 17:56
 */
public class Crisps implements IGoods {

    public void buyGood() {
        System.out.println("Hilox买了点薯片");
    }
}
/**
 * @author zhh
 * @description 巧克力
 * @date 2019-07-29 17:57
 */
public class Chocolate implements IGoods {

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

调整实现类

将面向实现编程中的 Hilox 类进行调整

public class Hilox {

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

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

测试类

public class Test {

    public static void main(String[] args) {
        Hilox hilox = new Hilox();
        hilox.buyGood(new Coke());
        hilox.buyGood(new Crisps());
    }
}

类结构图

说明

如果有其他商品接口的实现,只需要和上述可乐、薯片等商品实现类保持一致,平级扩展即可。而具体的 Hilox 实现类是不需要改动的。也就是说我们要面向接口编程,所写的扩展类是面向接口的而不是面向具体实现类。而对于具体购买什么商品是交给高层模块 Test 类来选择的。这样的话就做到了 Hilox 类和 Test 类之间的解耦,同时 Hilox 类和具体的商品实现类又是解耦的。

其他实现

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

构造器注入方式
  • 调整 Hilox 实现类
public class Hilox {

    private IGoods iGoods;

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

    public void buyGood() {
        iGoods.buyGood();
    }
}
  • 测试类
public class Test {

    public static void main(String[] args) {
        Hilox hilox = new Hilox(new Coke());
        hilox.buyGood();
    }
}
setter方法注入
  • 调整 Hilox 实现类
public class Hilox {

    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) {
        Hilox hilox = new Hilox();
        hilox.setiGoods(new Coke());
        hilox.buyGood();
    }
}

优点

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