接口导致死亡?

2025-06-09

接口导致死亡?

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

让我解释一下。

接口 101

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

简单地说,接口是一种契约,它规定一个类将具有某些特征(主要是公共方法和属性),其他组件可以与之交互。

接口的目的是为编程语言提供一定程度的解耦。例如,如果你有一个类需要读取一些外部数据,你可能会让它接收一个IDomainObjectDataProvider而不是SqlServerDomainObjectDataProvider

这样,您的类就不需要关心它是否正在与内存中、数据库中的数据对话,或者是否由某些外部 API 调用提供数据。

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

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

那么接口有什么问题呢?

接口本身并不,但它们确实有一些重大的权衡,而且我不相信开发人员充分考虑使用它们的权衡。

导航难题

首先,使用界面使得代码导航变得困难。

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

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

这听起来可能没什么,但如果你“顺其自然”地思考一个流程,这就好比你转过一个弯,发现你以为是门的地方却出现了一堵坚固的墙。你必须理解你所看到的东西,然后弄清楚你实际在处理哪些具体类型,找到它们,并找到相关的定义。

这对我们的生产力造成的损失很小,但却很严重,而且这种情况在界面丰富的环境中经常发生。

通过接口进行混淆

让我们回到接口作为契约的定义以及之前针对特定类型数据提供者的接口的示例。

确实,我们的代码非常灵活并且与特定的实现分离,这非常好。

但是如果我正在查看一个类并看到一个接口,我有时会忘记运行时的细节。

假设我们的IEmalSender应用程序中只有一种类型。如果我在浏览代码时看到的只是一个IEmailSender引用,我可能会忘记我们在生产环境中实际使用的发送者是什么,以及它的一些实现细节。

有些人可能会认为这是一件好事,我不应该关心,他们在某种程度上是对的,但问题在于,当我们在抽象方面思考太多时,就会很难看到具体的部署场景。

建筑水泥

我喜欢将界面视为软件开发中的一种“建筑水泥”。

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

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

这意味着,一个原本可能轻而易举就能完成的操作,现在却让我无法保持自然流畅,需要付出额外的努力和思考才能完成。虽然花费的时间不多,但足以让我三思而后行。

此外,如果接口的成员从未使用过,那么使用代码分析工具检测起来要比检测不遵循接口的方法困难得多。这意味着接口定义中的死代码会保留更长时间。

我的观点是,我们在维护软件的过程中,以一些小小的不便来支付接口费用。

虽然不是很多,但比你想象的要多,而且使用的接口越多,问题就越明显。

接口隔离原则

我发现接口的另一个主要问题是违反了接口隔离原则 (ISP)。ISP 是SOLID 编程原则的一部分,SOLID 编程原则包含 5 条原则,旨在确保软件能够长期维护。

具体来说,ISP 指的是优先考虑围绕专门任务的多个较小接口,而不是为执行许多一般任务的类设计一个较大的接口。

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

这有点简单且容易做到,因此阻力最小的路径会导致巨大的接口,例如,IUserRepository而不是较小的接口,例如IUserValidatorIUserCreator

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

  • 它们经常表现出前面章节列出的问题
  • 由于接口中成员的数量太多,很难进行新的实现
  • 它们往往是该接口的唯一具体实现
  • 它倾向于推广那些不遵守单一责任原则(SOLID的另一个原则)的课程

总而言之,大型接口不是一个好主意,并且往往会导致长期的维护难题。

继承与接口

那么,如果我对界面提出警告,我建议什么可能是更好的选择?

通常,当系统在实现上需要一定程度的灵活性时,它们并不需要像接口那样完全的灵活性。通常它们只需要一个基类,它可以作为一个小型的契约,用于依赖注入或测试。

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

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

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

当然,也存在一些缺点和权衡需要考虑:

  • 除非被覆盖,否则基类中的代码将出现在任何派生类中,这可能会对实现造成过多的限制
  • 你并不总是能够控制足够的代码来使基类成为可行的选择,或者层依赖性使这成为不可能
  • 如果类层次结构中已经存在继承,则这可能会导致“继承深度”过大。

因此,对于使用基类还是接口来说,这需要一点权衡。

一般来说,我喜欢使用接口来实现非常小的功能,并且倾向于使用基类来实现诸如配置控制反转容器之类的功能。

结束语

你的偏好将与你的需求相匹配。我只要求你不要自动假设“这应该是一个接口”或“这应该是一个基类”,甚至“我不应该将一个具体类传递给这个方法”。

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

任何事物都有其优点和缺点,而软件工程就是为你的代码库找到正确的组合。

文章“接口导致死亡?”首先出现在Kill All Defects上。

鏂囩珷鏉ユ簮锛�https://dev.to/integerman/death-by-interfaces-18ma
PREV
定义技术债务
NEXT
沟通技术债务业务视角代码分析与业务利益相关者沟通的技巧结束