接口隔离原则:接口里的方法,你都用得到吗?

Posted JavaEdge.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了接口隔离原则:接口里的方法,你都用得到吗?相关的知识,希望对你有一定的参考价值。

  • SRP
    一个类的变化来源应该是单一的
  • OCP
    不要随意修改一个类
  • LSP
    设计好类的继承关系。

我们强调面向接口编程,想实现OCP或DIP,都要依赖于接口实现。

接口不就是一个语法吗?把需要的方法都放到接口里面,接口不就出来了吗?
这种对于接口的理解,还只停留在语法层面。设计出来的只能算作是有了个接口,但想要设计出好接口,还要有在设计维度上的思考。

那什么样的接口算是一个好接口呢?这就需要我们了解接口隔离原则。

接口隔离原则

接口隔离原则,Interface segregation principle,ISP:不应强迫使用者依赖于它们不用的方法。
No client should be forced to depend on methods it does not use.

在接口中,不要放置使用者用不到的方法:

  • 站在使用度,这太合理了,我怎么可能爱上我不需要的方法呢?

  • 作为设计者,你肯定也同意

但实际设计时,却不见得都能记得了。

很多人分不清使用者,设计者是不同角色。很多人看来,接口的设计和使用是由同一人完成。这种角色区分意识的缺失,导致我们不能区分两种不同角色,这实质上也是没做好分离关注点。实际开发中,很多人其实两种角色都没有,他们根本没思考过接口的问题,因为他们更关心的是一个个具体类。只有到了必须的时候,接口才作为语法选项使用一次,这种做法干脆就是没思考设计。

然而,你不设计接口,并不代表没有接口。

在做软件设计的时候,我们经常考虑的是模型之间如何交互,接口只是一个方便描述的词汇,为了让我们把注意力从具体的实现细节中抽离出来。但是,如果没有设计特定的接口,你的一个个具体类就变成它的接口。同设计不好的接口一样,这样的“接口”往往也是存在问题的。

那接口设计不好会有什么问题呢?典型的问题就是接口过“胖”,什么叫接口过“胖”呢?我给你举个例子。

胖接口减肥

某银行的系统,支持存款、取款和转账。
通过一个接口向外部系统暴露这些能力,而不同能力的差异要通过请求内容区分。

所以,设计了一个表示业务请求的对象,像下面这样:

每种操作类型都对应着一个业务处理的模块,它们会根据自己的需要,去获取所需的信息,像下面这样:

收到请求后,业务分发即可:

看起来都很好,不少人也写得出来。
然而,在这个实现里,有一个接口就太“胖”了,它就是TransactionRequest。

TransactionRequest这个类包含了相关请求内容,虽无可厚非。但这里,我们容易直觉把它作为参数传给TransactionHandler。
于是,它作为一个请求对象,摇身一变,成了业务处理接口的一部分。

虽然你没有设计特定的接口,但具体类可以变成接口。不过,作为业务处理中的接口,TransactionRequest就显得“胖”了:

getDepositAmount方法只在DepositHandler 里使用;
getWithdrawAmount方法只在WithdrawHandler里使用;
getTransferAmount只在TransferHandler使用。
然而,传给它们的TransactionRequest却包含所有这些方法。

这有什么问题吗?
问题就在于,一个“胖”接口常常是不稳定的。比如说,现在要增加一个生活缴费的功能,TransactionRequest就要增加一个获取生活缴费金额的方法:

相应还需增加业务处理的方法:

虽然这种做法看上去还挺OCP,但实际上,由于TransactionRequest的修改,前面几个写好的业务处理类:DepositHandler、WithdrawHandler、TransferHandler都会受到影响。为什么这么说呢?

如果我们用的是一些现代的程序设计语言,你的感觉可能不明显。假如这段代码是用C/C++这些需要编译链接的语言写成的,TransactionRequest的修改势必会导致其它几个业务处理类重新编译,因为它们都引用了TransactionRequest。

实际上,C/C++的程序在编译链接上常常需要花很多时间,除了语言本身的特点之外,因为设计没做好,造成本来不需要重新编译的文件也要重新编译的现象几乎是随处可见的。

可理解为,如果一个接口修改了,依赖它的所有代码全部会受到影响,而这些代码往往也有依赖于它们实现的代码,这样一来,一个修改的影响就传播出去了。用这种角度去评估,你就会发现,不稳定的“胖”接口影响面太广。

怎样修改这段代码呢?既然这个接口是由于“胖”造成的,给它减肥就好了。根据ISP,只给每个使用者提供它们关心的方法。所以,我们可以引入一些“瘦”接口:

这里,把TransactionRequest变成了一个接口,目的是给后面的业务处理进行统一接口,而ActualTransactionRequest则对应着原来的实现类。我们引入了DepositRequest、WithdrawRequest、TransferRequest等几个“瘦”接口,它们就是分别供不同的业务处理方法使用的接口。

有了这个基础,我们也可以改造对应的业务处理方法了:

经过这个改造,每个业务处理方法就只关心自己相关的业务请求。那么,新增生活缴费该如何处理呢?你可能已经很清楚了,就是再增加一个新的接口:

然后,再增加一个新的业务处理方法:

对比两个设计,只有ActualTransactionRequest做了修改,而因为这个类表示的是实际的请求对象,在现在的结构之下,它是无论如何都要修改的。而其他的部分因为不存在依赖关系,所以,并不会受到这次需求增加的影响。相对于原来的做法,新设计改动的影响面变得更小了。

你的角色

回顾设计改进过程,重点在于,原本那个大的TransactionRequest被拆成若干小接口,每个小接口就只为特定的使用者服务。
好处在于,每个使用者只要关注自己所使用的方法,这样接口才可能稳定,“胖”接口不稳定的原因就是,它承担了太多的职责。

或许你从这个讨论里听出了一点SRP的味道,没错,你甚至可以把ISP理解成接口设计的 SRP。

ActualTransactionRequest实现了多个接口。在这个设计里面,每个接口代表着与不同使用者交互的角色,Martin Fowler将这种接口称为角色接口(Role Interface)。
就像每个人在实际生活中扮演着不同的角色。
在家里,我们是父母的子女
在公司里,我们是公司的员工
购物时,我们是顾客
出行时,我们是乘客。
但所有这些角色最终都是由我们一个人承担的。前面讲做接口设计时,我们虽然是一个个体,但常常要同时扮演设计者和使用者两个不同的角色。而在这段代码里,各种角色则汇聚到了ActualTransactionRequest。

在一个设计中,识别出不同角色至关重要,分离关注点!

接口是把变和不变隔离开。现在有ISP,接口应该是尽可能稳定。接口的使用者对于接口是一种依赖关系,被依赖的一方越稳定越好,而只有规模越小,才越有可能稳定。

还可从更广泛角度理解ISP,不依赖于任何不需要的东西。

之所以会依赖于这个数据库,是因为在技术选型时,我们用到了一个特定的框架,而这个框架缺省就依赖于这个数据库。开发人员为了快速实现,就把框架和数据库一起引入到了项目中,引发了后面的这些问题。

在高层次上依赖于不需要的东西,这和类依赖于不需要的东西异曲同工。

总结

不应强迫使用者依赖于它们不需要的方法。因为很多接口都设计得包含了太多内容。更好的设计是把大接口分解成一个个小接口。

这里的接口不仅是一种语法,所有的public方法都是接口。

我们在做接口设计时,需要关注不同的使用者。我们可以把ISP理解成接口设计的SRP。每个使用者面对的接口,其实都是一种角色接口。识别出接口不同的角色是至关重要的,这也与分离关注点的能力是相关的。

ISP还可以从更广泛的角度去理解,也就是说,不要依赖于任何不需要的东西,这个原则可以指导我们在高层次上进行设计。

除了接口太“胖”造成的问题,还有一个很重要的问题,它的依赖方向搞反了。我们下一讲就来讨论到底谁该依赖谁的设计原则:依赖倒置原则。

识别对象的不同角色,设计小接口。

以上是关于接口隔离原则:接口里的方法,你都用得到吗?的主要内容,如果未能解决你的问题,请参考以下文章

Java接口隔离原则:接口里的方法,你都用得到吗?

面向对象设计原则七:接口隔离原则

C#中啥叫接口隔离原则?

设计原则-ISP接口隔离原则

设计模式软件设计七大原则 ( 接口隔离原则 | 代码示例 )

设计原则之接口隔离原则