论接口的归属

标签: 架构设计

保留所有版权,请引用而不是转载本文(原文地址 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。这些信息是在接口中定义的,会直接传递给实现方法,是一种强约束。

因此,我们可以得到下面的图。

接口的归属划分

这时我们发现:

显然,后者才是我们追求的调用方和实现方的关系。

按照这种理解,调用方持有了接口,也就持有了它需要的服务。实现方则是服务的提供者,具体是由哪个具体实现提供,这不重要,只要符合调用方提供的接口规范即可。于是,这完美地实现了依赖倒置原则(Dependence Inversion Principle)的要求,避免了实现方变更的向上传递。如图所示。

接口属于调用方

3. 总结

通常,我们认为接口由实现抽象而来,即在调用方和实现方中,实现方是接口的生成者。然而这种理解是片面的。

接口,应该像是一种契约、一种需求,由调用方提出,并交由实现方给出具体的实现。这种思路利于保证接口契约的稳定,也利于实现模块间的松耦合。

本文首发于个人知乎:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。

作者书籍推荐