在实际的研发工作中,你是不是遇见过以下场景?
-
一个平台系统,需要接入各种各样的业务系统,而这些业务系统都有自己的账号体系,平台需要兼容这些系统的账号体系,于是代码中出现了大量依赖于各种账号体系的代码。
-
一个网站页面,需要越来越多的频道(首页、搜索、分类等),不同频道对应的个性化需求各不相同,并且各种页面的标准组件、布局、模板,以及与后端交互框架也各不相同,不同体系的代码依赖非常紧密。
-
一个通用的订单处理平台,各条业务线都需要通过这个平台来处理自己的交易业务,但是垂直业务线上的个性化需求太多,代码里随处可见定制化的需求代码。
对于这些问题,你可能已经有解决方案了:如果依赖和控制的东西过多了,就需要制定标准,反转控制,解耦,分层……甚至你也知道该如何在代码中解决这些问题,比如,面向接口编程,而不是面向实现编程。
在这个解决过程中,其实你已经在使用 DIP 了,可能对于 DIP 概念本身,还没有透彻理解,甚至说到 DIP 时感觉还很陌生。别担心,今天我就带你一起搞清楚 DIP,让 DIP 在你的开发工作中发挥更大的作用。
下面,我们就一起来看看依赖反转原则(Dependence Inversion Principle,简称 DIP)。
一、DIP:统一代码交互的标准
那应该怎么去理解“依赖反转”这个概念呢?其实,对于这种抽象性的概念,我很建议你结合现实生活中的场景或例子来剖析和理解。
比如,在没有电商的时代,商品交易时,通常是买家一手交钱、卖家一手交货,所以基本上卖家和买家必须强耦合(因为必须要见面交易)。而这时有一个中间商想出了一个更好的办法,让银行出面做交易担保——买家把钱先付给银行,银行收到钱后让卖家发货,买家收验货后,银行再把钱打给卖家。通过这样的方式,买卖双方把对对方的直接控制,反转到了让对方来依赖一个标准的交易模型的接口——银行。
这和浏览器的使用原理也很类似。浏览器(对应商品买家)并不依赖于后面的 Web 服务器(对应商品卖家),其只依赖于 HTTP 协议(对应银行),只要我们遵循 HTTP 协议就能在浏览器中提供很多丰富的 Web 功能,而不必针对特定的浏览器定制开发。
因此,我们可以总结一下,依赖反转原则(DIP)就是一种统一代码交互标准的软件设计方法。
回到 DIP 的概念上来,我们可以看一下它的原始定义:
-
高级组件不应依赖于低级组件,两者都应依赖抽象;
-
抽象不应该依赖实现,实现应该依赖抽象。
那应该如何去理解 DIP 这个原始定义呢?
首先,定义中的高级组件和低级组件,主要对应的是调用关系上的层级。比如,汽车油门(高级组件)调用汽车引擎(低级组件),但并不是说汽车油门就比汽车引擎更复杂、功能更完善、能力更高。再比如,软件程序都得依赖底层操作系统,而你不能说软件程序就一定比操作系统复杂。
其次,高级组件和低级组件都应依赖抽象,是为了消除组件间变化对对方造成的影响,换句话说,抽象是一种约束,让高级组件或低级组件不能太随意地变动。因为两者间有相互依赖关系,一方变化或多或少都会带给对方影响。比如,踩油门是加油,抬起油门是减油,这是一种抽象约束,只要约束不变,我们设计圆形油门还是方形油门都不会影响引擎的动力控制;反过来,引擎使用铝制还是铁制,也不影响引擎对油门加、减油的控制。
最后,抽象不应该依赖实现,实现应该依赖抽象。什么意思呢?这里我们拿 JDBC 这个数据库驱动协议作为例子来简单解释一下。在用 Java 开发增删改查的数据业务时,我们通常会开发一个数据库访问层——DAO 层,而它并不直接依赖于数据库驱动(实现),而是依赖于 JDBC 这个抽象。JDBC 并没有受不同数据库设计的影响,只要不同数据库驱动都实现了 JDBC,就能被 DAO 层所使用,而为了让应用程序使用,数据库驱动也依赖于 JDBC,这便是抽象不应该依赖实现,实现应该依赖抽象。
二、为什么要使用 DIP?
了解了 DIP 的概念及要点后,可能你会疑问为什么要使用 DIP,或者说 DIP 的使用意义有哪些。大致总结为如下两点:
-
可以有效地控制代码变化的影响范围;
-
可以使代码具有更强的可读性和可维护性。
使用 DIP 的第一个目的就在于:控制这种代码变化带来的影响。
比如,为了解决平台接入权限的问题,我们可以通过抽象一个账号权限体系的接口标准,让不同的业务系统按照这个统一标准来接入平台,同时我们的平台也按照这个标准来实现。此时,我们的内部系统和外部系统不再是通过定制化的映射来通信,而是使用了一套统一的标准接口来通信,只要接口不发生变化,即使外部系统发生了巨大变化,接入的功能并没有发生改变,这样就能有效地控制外部系统的变化对内部系统带来的影响。
使用 DIP 的第二个目的在于,增强代码的可读性和可维护性。该怎么去理解这个目的呢?
虽然现如今软件行业提倡“敏捷开发”,少写文档和注释,需求能提前上线更重要,但是,少写文档不是让我们不写文档和不写注释。比如,你接手两个项目,一个项目不仅没有系统设计文档,还没有代码注释,同时代码逻辑依赖又很多,到处都是看不懂的定制化逻辑;而另一个项目,只有少量文档,并且还有很清晰的接口定义和代码注释。通过这样的对比,你是不是立马就能知道哪个项目的维护难度更低?
使用 DIP,就是从设计上减少系统的耦合性,更能帮助厘清代码逻辑,因为代码是通过统一抽象之后,功能相同的处理都在同一个地方,所以,代码变得更加顺畅,更容易让人理解,也就增强了代码的可读性和可维护性。
三、如何给具体实现抽象标准接口
下面我们通过设计一个 Java 组件来演示其应用思路。
这个组件我们只实现一个功能:读取一串字符后,再输出显示。组件中我们定义 StringProcessor 类,该类使用 StringReader 组件获取 String 值,然后使用 StringWriter 组件将值写入输出流并打印。我们将 StringReader 类和 StringWriter 类统称为低级组件,StringProcessor 称为高级组件,这样能更清楚地了解每个设计选择是如何影响整体设计的。
1、设计一:低级组件和高级组件都作为具体类放在同一包中
StringProcessor 取决于 StringReader 和 StringWriter 的实现,读取字符并输出打印,整个逻辑实现在同一个包内通过三个具体类来完成,如下示例代码: