论接口的归属
保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/03/ )。
接口是系统架构设计中一种常用的抽象形式,它将功能的定义和功能的实现拆分开来。接口的使用也十分广泛,例如依赖倒置原则(Dependence Inversion Principle)便要求程序要依赖抽象接口,而不是依赖于具体的实现。
在本文我们探讨接口的归属问题,即接口是归属于接口调用方还是接口的实现方。这是一个纯概念性的讨论,但将会对软件的架构设计理念有着重要的意义。
1. 接口属于实现方?
通常,我们认为一个接口属于它的实现方。
因为,实现和接口共同组成了一个可以对外提供服务的模块整体,如下图所示。
这时,接口成为了实现的描述,调用方只需要通过接口便可以了解被调用模块的功能,而不需要分析其具体的实现细节。
而调用方则被排除在外,成了模块的外部系统。它能做的就是接受接口的定义,在此基础上才能依照接口的定义使用模块。
按照上述的理解,保证了接口和实现组成的被调用模块的内聚性,但却带来了一个严重的问题:变更传递。当模块发生变化时,接口和实现都会变化,这导致调用方不得不修改代码。当然,实现方也可以只修改实现而不修改接口,但这完全依赖实现方的仁慈。毕竟,接口是属于实现方的。
2. 接口属于调用方?
将接口划归调用方则可以解决变更传递的问题。而且,将接口划归调用方有以下依据。
2.1 接口的契约功能
接口实际可以理解为一个契约,契约的签约方是调用方和实现方。调用方承诺按照接口定义的方式来调用实现方;实现方承诺按照接口定义的方式来服务调用方。
但是,我们会发现上述契约并不是对等的。如果调用方违背契约,例如修改了方法名或者参数数目,是无法对应到具体实现的,因此,调用方不会违背契约。如果实现方违背契约,例如修改了方法的实现逻辑或者返回值,是很有可能的,因此,实现方可能会违背契约。可见,接口这个契约,主要强调的应该是实现方对调用方的承诺。对于这样的契约应该交由调用方保管。
或者换一种理解。实现方有修改接口(违背契约)的意愿,如果接口交由实现方管理,则接口很有可能会频繁变动,这就违背了契约精神。而将接口交由实现方管理,则可以保证接口的稳定。
按照这种思路,接口应该交由调用方管理,即划归调用方。而实现方则必须按照接口的定义来实现具体的功能。
2.2 接口的松耦合功能
接口是调用方的桩,这种桩的耦合往往是紧密的,例如下面的一个DAO接口中,显然RoleModel
是来自于调用方的Bean,也就是说接口本身依赖了调用方。
RoleModel queryByName(@Param("name") String name, @Param("appName") String appName);
接口是实现的抽象,同样是上述接口,其实现要确保返回值为RoleModel
,方法名为queryByName
,两个入参为String
。这些信息是在接口中定义的,会直接传递给实现方法,是一种强约束。
因此,我们可以得到下面的图。
这时我们发现:
- 如果将接口划归实现方,如虚线1所示。则实现方和调用方之间存在双向依赖。
- 如果将接口划归调用方,如虚线2所示。则调用方单向依赖实现方,实现方受到调用方给出的接口的强约束。
显然,后者才是我们追求的调用方和实现方的关系。
按照这种理解,调用方持有了接口,也就持有了它需要的服务。实现方则是服务的提供者,具体是由哪个具体实现提供,这不重要,只要符合调用方提供的接口规范即可。于是,这完美地实现了依赖倒置原则(Dependence Inversion Principle)的要求,避免了实现方变更的向上传递。如图所示。
3. 总结
通常,我们认为接口由实现抽象而来,即在调用方和实现方中,实现方是接口的生成者。然而这种理解是片面的。
接口,应该像是一种契约、一种需求,由调用方提出,并交由实现方给出具体的实现。这种思路利于保证接口契约的稳定,也利于实现模块间的松耦合。
可以访问个人知乎阅读更多文章:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。