通过接口死亡?
这可能是一个不受欢迎的观点,但是使用面向对象的编程语言中的接口可能弊大于利。
让我解释。
接口101
首先,消除歧义的时间:当我在这里谈论界面时,我指的是代码中的界面定义,而不是用户界面,用户体验或任何图形特性。
简而言之,接口是一种契约,说一个类将具有其他组件可以与之交互的某些特征-主要是公共方法和属性。
接口的目的是为编程语言提供某种程度的解耦。例如,如果您有一个需要读取一些外部数据的类,则可能会让它接受一个 IDomainObjectDataProvider
代替 SqlServerDomainObjectDataProvider
。
这样一来,您的价格就不必在乎是在与内存中的数据,数据库中的数据对话,还是由某些外部API调用提供的对话。
这是有道理的,并且是拥有接口的经典原因。
另一个原因可能是一层中的类没有引用另一层中定义的类。从这个意义上讲,接口可以提供一定程度的间接性。我不太喜欢这种推理,但在某些情况下可能是有效的。
那么接口出了什么问题?
界面本身并不坏,但是它们确实有一些重大的折衷,而且我不相信开发人员会充分考虑使用它们的折衷。
导航困境
首先,使用界面很难浏览代码。
如果我处于自己的开发环境中,则大多数人将支持单击控制键或某些键盘快捷键来导航到类型的定义。
使用具体类型,导航将直接带您到您最感兴趣的方法的实现。使用界面,您将导航到该方法的界面定义。
这听起来可能并不多,但是如果您“在流程中”并经过一个过程的思考,这类似于绕过一个拐角并找到坚固的墙,您希望在其中穿上一扇门。您必须先了解自己所看到的内容,然后找出您实际使用的具体类型,找到它们,然后找到相关的定义。
这对我们的生产力造成的损失很小,但意义重大,并且这种情况在界面丰富的环境中经常发生。
通过接口进行混淆
让我们回到将接口定义为合约的角度,以及前面针对特定类型的数据提供者的接口示例。
没错,我们的代码灵活且与特定的实现脱钩是非常好的。
但是,如果我正在查看一个类并看到一个接口,有时可能会失去对运行时细节的跟踪。
假设我们只有一种 IEmalSender
例如在应用程序中。如果我在浏览代码时看到的只是一个 IEmailSender
参考,我可能无法跟踪生产中实际使用的发件人以及其实现的某些细节。
有人可能会认为这是一件好事,我不必理会,而且它们在一定程度上是对的,但问题出在我们从抽象的角度考虑太多,以至于很难看到具体的部署方案。
建筑水泥
我喜欢将接口视为软件开发中的一种“体系结构水泥”。
我的意思是,如果我正在做一些重构(在不更改其行为的情况下清理代码的形式)并且发现我不再需要传递某个参数,或者我想使一个异步的方法同步(反之亦然),或任何数量的细微调整,使此操作变得更加困难。
我不必导航到一个地方,而必须导航到该界面并在那里进行更改。如果该接口还有其他实现,我需要找出它们并确保也进行了更改。
这意味着,过去可能微不足道的一项操作现在将我带离了我的自然境界,并且需要额外的努力和思想来执行。可能不算很多,但足以让我三思。
此外,如果从未使用过接口的成员,那么使用代码分析工具检测这种情况要比不遵循接口的方法难得多。这意味着作为接口定义一部分的无效代码的停留时间更长。
我的意思是,我们在软件维护期间为接口支付的费用很少。
数量不多,但是比您想的要多,而且使用的接口越多,问题就越明显。
接口隔离原理
我在接口上看到的另一个主要问题是违反了接口隔离原理(ISP)。 ISP是SOLID编程原则的一部分,这是随着时间的推移生产可维护软件的5条原则。
具体地说,ISP谈论的是在专门任务周围选择许多较小的接口,而不是为执行许多常规操作的类设计的较大接口。
当开发人员向现有系统添加接口时,经常会违反该原理。通常,他们会进入一个类并为所有公共成员提取一个接口,然后用该接口的用法替换该类的用法。
它有点简单易行,因此阻力最小的路径导致了巨大的界面,例如 IUserRepository
而不是像 IUserValidator
和 IUserCreator
。
这些较大的接口存在许多问题,包括:
- 他们经常演示前面几节中列出的问题。
- 由于接口中成员的数量,它们使制作新的实现变得困难
- 它们往往是该接口的唯一具体实现
- 它往往会促进不遵守“单一责任原则”(SOLID的另一个承租人)的价格
总而言之,大型接口不是一个好主意,从长期来看,它往往会导致维护方面的麻烦。
继承与接口
因此,如果我在界面上提出警告,那么我建议使用哪种方法更好?
通常,当系统在实现上需要一定程度的灵活性时,它们并不需要界面提供的完全灵活性。通常,他们只需要一个基类,就可以将其作为依赖项注入或测试的微型合约。
因此,我主张在您考虑添加接口时,应该考虑引入或使用现有的基类是否更合适。
基类可以提供的一些优点:
- 导航到基类实际上可以导航到相关方法的具体或默认实现
- 基类提供一定程度的代码重用/共享,这是无法通过接口实现的
- 基类比接口稍微容易重构
当然,有一些缺点和折衷考虑:
- 您的基类中的代码将出现在任何派生类中,除非被覆盖(否则可能会严重限制实现)
- 您不一定总是控制足够的代码以使基类成为可行的选择,否则图层依赖关系将使这成为不可能
- 如果您的类层次结构中已经在进行继承,则可能导致过多的“继承深度”。
因此,就使用基类还是接口而言,这是一个权衡。
总的来说,我喜欢将接口用于非常小的功能,并且倾向于将基类用于配置控制容器的倒置之类的事情。
总结思想
您的喜好将满足您的需求。我要问的是,您不仅会自动假设“这应该是一个接口”或“这应该是基类”,甚至“我不应该将具体的类传递给此方法”。
是否针对灵活性,维护,快速开发或其他方面进行优化完全取决于您。
一切都有优点和缺点,软件工程就是要为您的代码库找到合适的组合。
通过接口死亡吗?首先出现在《杀死所有缺陷》上。