基于接口而非实现编程

什么是面向接口编程
“基于接口而非实现编程”这条原则的英文描述是:“Program to an interface, not an implementation”。

我们理解这条原则的时候,千万不要一开始就与具体的编程语言挂钩,局限在编程语言的“接口”语法中(比如Java中的 interface)。这条原则是一条比较抽象、泛化的设计思想。

实际上,理解这条原则的关键,就是理解其中的“接口”两个字,这里的“接口”是泛指,可以理解为“抽象”。“基于接口而非实现编程”这条原则的另一个表述方式是“基于抽象而非实现编程”,后者的表述方式其实更能体现这条原则的设计初衷。落实到具体的编码,“接口”可以理解为编程语言中的接口(interface)或者抽象类(abstract class)。

面向接口编程的好处
在我看来,面向接口编程可以带来两大好处:解耦和提供扩展性。面向接口编程可以将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低耦合性,提高扩展性。

解耦
软件设计与开发最重要的工作之一就是应对复杂性,而控制代码复杂性最关键的就是解耦。对依赖关系复杂的软件作解耦的常用手段就是抽象和增加中间层,无论哪种手段,都体现了基于抽象而非实现编程的思想。

一个真实的典型耦合案例,就是java的logger,早些年,大家都用commons-logging、log4j并没有什么问题。然而,此处埋了一个雷—那就是对logger实现的强耦合。当logback与log4j2出现后,情况变得复杂,程序员发现想要切换logger实现的代价非常高,为了减少应用层代码的改动,尽可能最小代价地完成logger实现切换,不得不上各种Bridge(适配器模式),到最后日志体系代码搞得极其复杂,没什么人看得懂。

试想如果能够时光倒流,我们一开始就在应用与日志实现之间加入一层抽象,让两者解耦,应用层基于日志的抽象编程而非某一个具体的日志实现。MyLogger接口这个关键抽象将接口和实现相分离,封装了不稳定的实现,暴露稳定的接口。应用层面向稳定的MyLogger接口而非实现编程,不依赖不稳定的实现细节,这样当底层的Logger实现发生变化的时候,应用层的代码基本上不需要做改动,显著降低耦合性。

提供扩展性
可扩展设计,主要是利用了面向对象的多态特性,面向接口编程的思想将接口和实现分离,实现可以有N多种,上层可以按需加载最合适的那一个实现,这恰恰是良好扩展性的体现。

扩展性诉求在软件开发中无处不在,例如我要将系统产生的数据存储起来放置到如Oracle的数据库中,后来出现了IOE运动,我又要把数据放置到免费开源的MySQL数据库中。如果软件是直接基于底层存储为Oracle这个“现实”去编写的,当需要切换新的存储源时,就必须要修改应用代码了。更恰当的做法应该是定义一组接口,让不同的数据库厂商实现去实现这些接口,从而在切换配置实现的时候,应用代码不再需要更改了。

JDBC(Java Database Connection)就为Java开发者使用数据库提供了统一的编程接口,它由一组Java类和接口组成,是Java程序与数据库通信的标准API。只要数据库想要和Java连接的,数据库厂商必须自己实现JDBC这套接口,数据库厂商的JDBC实现,我们就叫它此数据库的驱动。

img

装载MySQL驱动:Class.forName(“com.mysql.jdbc.Driver”);
装载Oracle驱动:Class.forName(“com.jdbc.driver.OracleDriver”);

面向接口编程实现依赖倒置
依赖倒置原则的英文翻译是Dependency Inversion Principle,缩写为DIP,依赖倒置原则的含义有两层:

  • 高层模块不要依赖低层模块,高层模块和低层模块应该通过抽象(abstractions)来互相依赖。
  • 抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)要依赖抽象(abstractions)

这里“抽象”这个词是不是很熟悉,前面我们说了“基于接口而非实现编程”的真实含义就是“基于抽象编程”,而实现DIP的关键就是定义抽象,去依赖抽象而非实现(实现就是detail,是细节),所以实现DIP的手段无它,就是面向接口编程。

DIP规则中提到了高层模块和低层模块,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。在平时的业务代码开发中,高层模块依赖底层模块是没有任何问题的,例如MVC结构的工程,从高到低,Controller层调用Service层,Service层调用Repository层,每一层各司其职。DIP原则主要用来指导框架型软件的设计。

我们拿Tomcat这个Servlet容器作为例子来解释一下,Tomcat是运行Java Web应用程序的容器。我们编写的Web应用程序代码只需要部署在Tomcat容器中,便可以被Tomcat容器调用执行。按照上面提到的划分原则,Tomcat就是高层模块,我们编写的Web应用程序代码就是低层模块。Tomcat和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是Servlet规范。Servlet规范只是定义并输出标准,不提供细节。而Tomcat容器和Web应用程序都去依赖Servlet规范,两者解耦,都去依赖抽象,各自独立发展,只要遵守规范即可。因此容器可以有很多种,例如Jetty与JBoss也是应用广泛的Servlet容器,而Spring可以算得上是最知名的实现了Servlet规范的Web应用程序开发框架了,它帮助程序员把很多可复用的底层工作都做好了。