01.21 使用接口是好的设计,还是弊大于利?

这可能是一个不受欢迎的观点,但是使用面向对象的编程语言中的接口可能弊大于利

使用接口是好的设计,还是弊大于利?

Photo by Clint Patterson on Unsplash

让我解释。

接口101

首先,消除歧义的时间:当我在这里谈论接口时,我指的是代码中的接口定义,而不是用户界面,用户体验或任何图形特性。

简而言之,接口是一个契约,说一个类将具有某些特性,主要是公共方法和属性,其他组件可以与之交互。

接口的目的是为编程语言提供某种程度的解耦。

例如,如果您有一个需要读取一些外部数据的类,则可以让它接受一个IDomainObjectDataProvider而不是SqlServerDomainObjectDataProvider。

这样一来,您的课程就不必在乎是与内存中的数据,数据库中的数据对话还是由某些外部API调用提供的对话。

这是有道理的,这是拥有接口的经典原因。

另一个原因可能是一层中的类没有引用另一层中定义的类。 从这个意义上讲,接口可以提供一定程度的间接性。 我不太喜欢这种推理,但在某些情况下可能是有效的。

接口有什么问题?

界面本身还不错,但是它们确实存在一些重大的折衷,而且我不相信开发人员会充分考虑使用它们的折衷。

导航困境

首先,使用IDE很难浏览代码。

如果我处于自己的开发环境中,则大多数人将支持单击控制键或某些键盘快捷键来导航到类型的定义。

使用具体类型,导航将直接带您到您最感兴趣的方法的实现。使用界面,您将导航到该方法的界面定义。

这听起来可能并不重要,但是如果您"在流程中"并经过一个过程的思考,这类似于绕过一个拐角并找到坚固的墙,您希望在其中有一扇门。

您必须先了解自己所看到的内容,然后找出您实际使用的具体类型,找到它们,然后找到相关的定义。

这对我们的生产力造成的损失很小,但意义重大,并且这种情况在界面丰富的IDE中经常发生。

通过接口进行混淆

让我们回到将接口定义为合同的角度,以及前面针对特定类型的数据提供者的接口示例。

没错,我们的代码灵活且与特定的实现脱钩非常好。 但是,如果我正在查看一个类并看到一个界面,有时可能会失去对运行时细节的跟踪。

例如,假设我们在应用程序中只有一种类型的IEmalSender。 如果我在浏览代码时看到的只是IEmailSender引用,则可能无法跟踪生产中实际使用的发件人及其实现的某些细节。

有人可能会认为这是一件好事,我不必理会,而且它们在一定程度上是对的,但问题出在我们从抽象的角度考虑太多,以至于很难看到具体的部署方案 。

架构水泥

我喜欢将接口视为软件开发中的一种"架构水泥"。

我的意思是,如果我正在做一些重构(在不更改其行为的情况下清理代码的形式)并且发现我不再需要传递某个参数,或者我想使一个异步的方法同步 (反之亦然),或任何数量的细微调整,使此操作变得更加困难。

我不必导航到一个地方,而必须导航到该接口并在那里进行更改。 如果该接口还有其他实现,我需要找出它们并确保也进行了更改。

这意味着,过去可能微不足道的一项操作现在将我带离了我的上下文,并且需要额外的努力和思想来执行。 可能不算很多,但足以让我三思。

此外,如果从未使用过接口的成员,那么使用代码分析工具检测这种情况要比不遵循接口的方法难得多。 这意味着作为接口定义一部分的无效代码的停留时间更长。

我的意思是,我们在软件维护期间为接口支付的费用很少。

数量不多,但比您想的要多,而且使用的接口越多,问题就越明显。

接口隔离原理

我在接口上看到的另一个主要问题是违反了接口隔离原理(ISP)。 ISP是SOLID编程原则的一部分,这是随着时间的推移生产可维护软件的五项原则。

具体地说,ISP谈论的是在专门任务周围选择许多较小的接口,而不是为执行许多常规操作的类设计的较大接口。

当开发人员向现有系统添加接口时,经常会违反该原理。 通常,他们会进入一个类并为所有公共成员提取一个接口,然后用该接口的用法替换该类的用法。

它有点简单易行,因此阻力最小的路径导致了诸如IUserRepository之类的大型接口,而不是诸如IUserValidator和IUserCreator之类的较小接口。

这些较大的接口存在许多问题,包括:

· 他们经常演示前面几节中列出的问题。

· 由于作为接口一部分的成员数量,它们使制作新的实施变得困难。

· 它们往往是该接口的唯一具体实现。

· 它往往会促进不遵守"单一责任原则"(SOLID的另一个承租人)的类。

总而言之,大型接口不是一个好主意,从长期来看,它往往会导致维护方面的麻烦。

继承与接口

因此,如果我在界面上提出警告,那么我建议使用哪种方法更好?

通常,当系统在实现上需要一定程度的灵活性时,它们并不需要接口提供的完全灵活性。 通常,他们只需要一个基类,就可以作为依赖项注入或测试的微型合同。

因此,我主张在您考虑添加接口时,应该考虑引入或使用现有的基类是否更合适。

基类可以提供的一些优点:

  • 导航到基类实际上可以导航到相关方法的具体或默认实现。
  • 基类提供一定程度的代码重用/共享,这是无法通过接口实现的。
  • 基类比接口稍微容易重构。

当然,有一些缺点和折衷考虑:

  • 您的基类中的代码将出现在任何派生类中,除非被重写,否则可能会严重限制实现。
  • 您不一定总是控制足够的代码以使基类成为可行的选择,否则图层依赖性使这成为不可能。
  • 如果您的类层次结构中已经在进行继承,则可能导致过多的"继承深度"。

因此,就使用基类还是接口而言,这是一个权衡。

总的来说,我喜欢将接口用于非常小的功能,并且倾向于将基类用于配置控制容器的反转之类的事情。


总结思想

您的喜好将满足您的需求。 我要问的是,您不会自动假设:"这应该是一个接口"或"这应该是基类",甚至"我不应该将具体的类传递给此方法"。

是否针对灵活性,维护,快速开发或其他方面进行优化完全取决于您。

一切都有优点和缺点,软件工程就是要为您的代码库找到合适的组合。


(本文翻译自Matt Eland的文章《Death by Interfaces》,参考:https://medium.com/better-programming/death-by-interfaces-ec7b35e634c1)


分享到:


相關文章: