关于接口和依赖的一些想法

jiekouyilai
关于接口和依赖的一些想法

开发人员经常提到2个词,“依赖”和“耦合”。最近的工作是一个系统的重构,在这方面想得比较多,在此总结一下

对模块的理解

模块可以在逻辑和物理2个层面上进行划分

对于比较小的工程,可能会把所有的模块都放在一个工程里。这样的话,不同的模块仅仅是在逻辑上有区别,在物理上还是一致的,因此也就不存在由于依赖而无法编译的问题

对于比较大的工程,除了在逻辑上划分之外,不同的模块往往还会放在不同的工程中,这时理顺模块之间的依赖关系,就尤为重要。因为如果存在依赖缺失,或者循环依赖等问题的话,整个项目就无法编译

对依赖和耦合的理解

首先我认为,在开发层面,设计是为了解决职责分配、依赖和耦合的问题,本文仅讨论依赖和耦合

只要2个模块需要协同工作来完成一个功能,这2个模块就一定存在依赖和耦合。如果2个模块完全独立,没有任何依赖,实际上这2个模块就没有关系,没必要放到一起说 有些场景,2个模块在代码层面没有任何依赖,而是通过数据库、文件、JMS等其它方式来集成,那么可以认为这2个模块不存在“编译依赖”,但是依赖依然存在。假如模块A不再遵守约定的JMS消息格式,那么模块B实际上也无法继续正常使用 因此,模块之间只有紧耦合和松耦合的区别,不存在完全不耦合的模块,除非它们毫无关系。我们设计的目的,也就是使2个模块之间的依赖尽可能地减少,达到松耦合

某种意义上,我觉得耦合和依赖是同一个概念

画2个方框,分别代表模块A和模块B,有一种比较直观的方法,来判断这2个模块的耦合程度和依赖情况:如果模块A里大量依赖了模块B中的代码,那么可以简单地认为,这2个模块是紧耦合的;如果模块B仅通过若干个清晰的接口暴露给模块A,那么就认为,这2个模块是松耦合的

比如说,存在一个医院就诊系统,模块A是患者,模块B是医院行政部门。那么就存在2种设计。

一种是患者直接依赖模块B中的挂号、诊室、药房、收银。另一种是采用Facade设计模式,患者仅仅依赖模块B中的“接待”。 采用前者,模块A就至少依赖了模块B的4个类,或者说,模块A了解模块B的实现细节。采用后者的话,模块A仅仅依赖模块B的1个类,或者说,模块A对模块B的实现细节是一无所知的。这2种设计放在一起比较,很明显是后者优于前者,而且后者的依赖关系也更加清晰

对接口的理解

首先,我认为不能狭义地把接口理解为interface关键字。如前文所说,有些模块或者子系统之间,是通过JMS来集成的。不管双方内部是如何实现,最终通过JMS Message来传递消息。这种情况下,就认为JMS消息是模块间的接口

关于接口,有很多说法,比如“不要针对实现编程,要针对接口编程”、“没有接口,就没有设计”等等。对于这些说法,每个人都有不同的理解,我个人对于接口有以下看法:

有利于多模块协同开发

接口可以认为是模块之间的契约。在开发之初,首先把接口确定下来。然后各模块就可以各自开发,只要保证遵守接口的契约即可。 比如说,2个模块通过JMS集成,那么只要把消息格式确定下来,2个模块的开发就相互不影响了 再比如说,模块A依赖模块B的InterfaceB接口,那么只要InterfaceB确定下来,模块A就可以自行进行内部的开发,不用去关心模块B的实现情况。同时也解决了编译依赖的问题,因为模块A除了InterfaceB之外,不依赖模块B的任何代码

有利于清晰模块间的依赖

前面说过,协同工作的模块之间肯定存在依赖关系。但是到底怎么依赖,就很有讲究,不同的依赖方式,肯定会造成松耦合和紧耦合的区别

以经典的创建对象为例,从直接new实例,到工厂,到现在的依赖注入,耦合程度不断降低 以下代码都在模块A中,假设模块A用到了模块B的Car的实例

1
Benz car = new Benz();

这种方式显然是最差的,因为依赖的是具体的实现类,如果后面换了一种车型,不但客户端(模块A)需要修改,而且依赖关系完全是不稳定的。现在依赖Benz类,后面可能就依赖BMW类

1
Car car = new Benz();

这种方式稍微好了一点,有了接口的定义,至少调用方法是稳定的了,但是还是没有解决依赖不稳定的问题

1
Car car = CarFactory.getInstance("Benz");

这种方式就比前一种更进了一步,不但调用方法稳定,而且依赖也是稳定的,现在不管模块B有多少种Car的实现类,模块A都仅仅依赖于Car接口和CarFactory类。但是依然是由客户端来负责创建,当模块B变更了实现类,客户端的代码依然需要修改

1
2
@Autowired
private Car car;

这就是现在的依赖注入,除了具备上述工厂模式的优点之外,连创建实例的职责,也转移到了依赖注入框架,模块A和模块B的依赖达到了最低(当然如上文所说,依赖仍然存在) 依赖接口,而不是依赖实现类,这种依赖比较稳定,而且范围也比较明确

模块内部比较稳定的类之间,不需要接口

有一种做法,就是很死板地规定一个实现类对应一个接口,哪怕在模块内部,也通过接口来调用,这种方式我倒是持保留意见,暂时没看到有什么好处

在三层架构中,依赖接口编程

这个和上一条有点类似,大部分项目里,在DAO层,都会有一组DAO接口对应一组DAO实现,但是实际上,替换DAO的实现类倒是几乎不会发生 但是因为这层接口,可以作为2层之间的界限,所以我个人觉得还是有意义的,可以保留