为什么你必须关心一个对象引用是接口还是类?

9
我经常遇到关于接口类型名称是否应添加某种前缀/后缀约定的讨论,通常在名称前面添加"I"。
个人认为不需要前缀,但这不是本问题的重点。相反,它是关于我经常听到的争论之一:
您无法一目了然地看到某个东西是接口还是类。
我脑海中立刻出现的问题是:除了对象创建之外,您为什么要在意对象引用是类还是接口?
我已将此问题标记为语言无关,但正如已经指出的那样,可能并非如此。我认为是因为虽然特定的语言实现细节可能很有趣,但我希望将其保持在概念层面上。换句话说,我认为,在概念上,您永远不必关心对象引用是作为类还是接口进行了类型化,但我不确定,因此提出了这个问题。
这不是关于IDE及其在可视化不同类型时所做或不做的讨论;当浏览代码(包/源/任何形式)时,关心对象的类型确实是必要的。这也不是关于任何命名约定的利弊的讨论。除了对象创建之外,我似乎无法想象其他情况下您需要关心是否引用了具体类型或接口。

2
我不在意“一眼识别接口”的事情。我更烦恼的是“Impl”后缀的问题,这往往被那些不用“I”前缀命名接口的人用于第一个具体实现。对我来说,这比“I”还要丑陋。 - JasonTrue
JasonTrue:精彩地概括了我的想法。 - Steve Ellinger
@JasonTrue,我完全同意。 - Marcus Stade
9个回答

10

大部分情况下,你可能不太关心这个。但有一些情况下你会关心。这种情况有几种,具体取决于语言。有些语言对此并不在意。

在控制反转(通过参数传递)的情况下,对于调用其方法等,你可能并不关心它是一个接口还是一个对象。但在处理类型时,这肯定会有所区别。

  • 在像.NET这样的托管语言中,接口通常只能继承一个接口,而类可以继承一个类但实现多个接口。类与接口的顺序在类或接口声明中也可能很重要。因此,在定义新的类或接口时需要知道哪个是哪个。

  • 在Delphi/VCL中,接口是引用计数和自动收集的,而类必须显式释放,因此整个生命周期管理都受到影响,而不仅仅是创建。

  • 接口可能不是类引用的可行来源。

  • 接口可以转换为兼容的接口,但在许多语言中,它们不能转换为兼容的类。类可以转换为任何一个。

  • 接口可能会被传递到IID或IUnknown类型的参数中,而类则不能(除非进行强制转换和支持接口)。

  • 接口的实现是未知的。其输入和输出是定义的,但创建输出的实现则被抽象化了。一般来说,当使用类时,人们可能知道类的工作原理。但在使用接口时,不应做出这样的假设。在一个完美的世界中,这可能没有影响。但在现实中,这肯定会影响你的设计。


注意:保留了原文中的HTML标签。

  1. 不确定你指的是什么。
- Marcus Stade
考虑到编程实践中的接口优先于实现,这难道不是一件好事吗?如果可以将类型转换为接口,为什么还要将其转换为类呢?在这种情况下,仍然不确定您为什么关心具体类型。 - Marcus Stade
我完全理解并赞赏您对此的务实看法,但这不是代码异味的迹象吗?还是只是生活中不幸的事实? :) - Marcus Stade
这并不仅仅是意识问题。我认为这并不理想,但这是生活的事实。我的观点是,你应该编写接口代码(无论你有一个接口还是具体类型),最好尽可能少地了解实现细节,大多数情况下都可以做到。但实际上,你是否拥有接口是我在这里要解决的问题。我的观点仅供修辞参考。我认为你和我在接口的理想使用上大多数时候是一致的。 - Phil Gilmore
感谢您抽出时间参与讨论。我会接受这个答案,因为它为一个有点抽象的问题增加了一个非常实用的视角。非常感谢您抽出时间写下这篇回答! - Marcus Stade
显示剩余4条评论

1

具体类可以拥有但接口不能的东西:

  1. 构造函数
  2. 实例字段
  3. 静态方法和静态字段

因此,如果您使用以“ I”开头的约定来命名所有接口名称,则向您的库的用户指示特定类型不会具有上述任何内容。

但是,我个人认为这不足以理由来以“ I”开头命名所有接口名称。现代IDE已经足够强大,可以指示某些类型是否为接口。此外,它还隐藏了接口名称的真正含义:想象一下如果Runnable和List接口分别被命名为IRunnable和IList。


具体类不一定拥有这些东西。很难想象在不关心细节的情况下,您会关心类型是否拥有这些东西的任何情况,因此无论如何,您都必须阅读文档。 - Porculus
Java接口可以具有“静态字段”:public interface MagicNumber { public static final int L_U_E = 42; } 在Java实际拥有枚举之前,这些常见于“类型安全的枚举模式”。 - Stephen P
构造函数仅在对象创建时必要(当然),使用 DI 可以有效地抽象掉对象构建,使这一点无关紧要。实例字段是实现细节,应该封装起来。为了暴露,使用属性。静态字段如果不是只读常量,则意味着全局状态,这被认为是一种不好的做法。修改状态的静态方法也因同样的原因被认为是一种不好的做法。即便如此,在支持此功能的语言中,静态方法也可以使用委托来表示。 - Marcus Stade

1

当使用一个类时,我可以做出这样的假设,即我将从相对较小且几乎有明确定义的子类范围中获得对象。这是因为子类化是一个不应该轻易做出的决定,特别是在不支持多重继承的语言中。相比之下,接口可以被任何类实现,并且实现可以稍后添加到任何类中。

因此,这些信息在浏览代码并尝试了解代码作者想要做什么时非常有用 - 但我认为如果 IDE 显示接口/类作为独特的图标,那就足够了。


不确定你所说的“获取对象”是什么意思。这听起来像是你要在类中键入依赖项,而这恰恰是我认为你不应该做的事情。当浏览代码时,你肯定正确地强调了区分类型的必要性,但是任何现代 IDE 都应该在 UI 中处理好这个问题。问题是,你为什么要关心你的对象引用是针对类还是接口进行了类型划分?无论如何,感谢您的答案! - Marcus Stade
@macke:所谓“获取对象”,是指任何地方,查看任何代码行,而不仅仅是方法签名中。现在,当我查看一行代码时,可以问自己:类的类型层次结构(特别是在没有多重继承的语言中)意味着强烈的分类,比如“它首先是一个人”。而接口只是说“顺便提一下,这个对象也实现了这些方法”。就像阅读一本书一样:我们不断地尝试理解上下文,并利用任何提示来问自己:作者可能意味着什么? - Chris Lercher
另一件事是,如果FooBar是类类型,那么任何类型是否都能满足这两个约束条件,完全取决于其中一个类型是否派生自另一个类型。相比之下,如果其中一个或两个是接口类型,那么除非另一个是密封类型,否则通常必须检查系统中的所有类型,以确保没有类型能够同时满足这两个约束条件。 - supercat

1

我同意你的观点(因此接口不使用“I”前缀)。我们不应该关心它是抽象类还是接口。

值得注意的是,Java之所以需要接口的概念,仅是因为它不支持多重继承。否则,“抽象类”概念就足够了(可以是“所有”抽象的,或部分抽象的,或几乎具体的,只有1个微小的抽象部分等)。


1

你想一眼看出哪些是“接口”和哪些是“具体类”,这样你就可以将注意力集中在设计中的抽象上,而不是细节。

好的设计基于抽象 - 如果你知道并理解它们,你就能够理解系统,而不需要了解任何细节。因此,在理解代码时,你知道可以跳过没有I前缀的类,并专注于那些有I前缀的类,同时你也知道要避免围绕非接口类构建新代码,而无需参考其他设计文档。


我不确定这是否回答了问题。尽管您提倡关注设计而不是具体细节的良好实践,但我不明白在这个意义上关心您的对象引用是接口还是类有任何区别。 - Marcus Stade
1
在我选择的编程语言(C++)中,“接口”和“具体类”实际上没有什么区别 - 我不介意一个没有纯虚方法的接口。然而,我确实希望能够清楚地看到哪些类被设计为“接口”(概念上的),以区分那些只是“具体实现”的类。 - Joris Timmermans
感谢您的深入评论,我已经点赞了。我想我的问题可能应该限制在实现接口上下文的语言上,从而更加清晰明了。无论如何,我非常感谢您的洞察力,谢谢! - Marcus Stade

1

我同意I*命名约定在现代面向对象语言中已经不再适用,但事实上这个问题并不是真正与编程语言无关。有些情况下你会出于没有实现或没有访问权限等原因创建一个接口而不是出于架构原因。对于这些情况,你可以将I*读作*Stub或类似的概念,并且在这些情况下创建IBlah和Blah类是有意义的。

然而,如今这种情况很少遇到,在现代面向对象语言中,当你说“接口”时,你实际上想要的就是“接口”,而不仅仅是“我没有这段代码”。所以没有必要使用I*,事实上它还会鼓励糟糕的面向对象设计,因为你将无法获得自然命名冲突来告诉你架构出了问题。比如,如果你有一个List和一个IList... 它们有什么区别?你何时会使用它们之一?如果你想要实现IList,你是否会受到List的概念约束?让我告诉你... 如果我在我的任何代码库中都找到了IBlah和Blah类,我会随机删除其中一个,并剥夺那个人的提交权限。


只是为了继续抱怨一下,使用依赖注入时,您甚至不需要在对象创建时关心它。 - CurtainDog
喜欢这个发泄,即使它并没有真正回答我的问题。 :) - Marcus Stade
你可能是对的,这个问题并不是与语言无关的,但我想不到更好的标签,因为我正在讨论接口的概念,而不管语言实现。某些实现显然可能会强加需要关注的细节,比如Phil Gilmore关于Delphi的注释。我认为在概念层面上,您永远不需要关心您的引用是作为接口还是类进行类型化,但我不确定,因此提出了这个问题。 - Marcus Stade

0

你应该关注这个问题,因为:

  1. 在接口名称中使用大写字母"I",使得你或你的同事能够使用实现该接口的任何实现。如果将来你想出了一种更好的方法来做某件事情,比如一个更好的列表排序算法,你将不得不改变所有调用方法。

  2. 它有助于理解代码 - 例如,你不需要记住所有10个I_SortableList的实现,你只需要知道它对列表进行排序(或类似的操作)。你的代码变得几乎是自我记录的。

为了完成讨论,这里是一个伪代码示例,说明了上述内容:

//Pseudocode - define implementations of ISortableList
Class SortList1 : ISortableLIst, SortList2:IsortableList, SortList3:IsortableList

//PseudoCode - the interface way
void Populate(ISortableList list, int[] nums)
{
    list.set(nums)
}

//PseudoCode - the "i dont care way"

void Populate2( SortList1 list, int[] nums )
{
    list.set(nums)
}

...
//Pseudocode - create instances

SortList1 list1 = new SortList1();
SortList2 list2 = new SortList2();
SortList3 list3 = new SortList3();

//Invoke Populate() - The "interface way"
Populate(list1,nums);//OK, list1 is ISortableList implementation
Populate(list2,nums);//OK, list2 is ISortableList implementation
Populate(list3,nums);//OK, list3 is ISortableList implementation

//Invoke Populate2() - the "I don't care way"
Populate(list1,nums);//OK, list1 is an instance of SortList1
Populate(list2,nums);//Not OK, list2 is not of required argument type, won't compile
Populate(list3,nums);//the same as above

希望这可以帮到你,
Jas.

我不确定你是否理解了问题。编程到接口是一种好的实践,我完全赞同。问题是,作为对象实例的消费者,您为什么要关心您的引用是接口还是类?接口仍然是接口,无论它们是否以'I'开头,老实说,只要命名约定是一致的,命名约定并不重要。我希望这能稍微澄清一些事情! - Marcus Stade
我不明白如果没有参考可扩展性原则(“设计以便变更”)和重用,如何思考你的问题。想象一下扩展一个对象工厂。如果你的团队中的任何人都必须来回检查MyObj是接口/抽象类而不是实现,他们将如何为你的接口添加对另一个实现的支持(对于相当复杂的商业系统可能达到50个)?这就是为了避免过度使用继承(白盒重用)而需要做的事情。祝一切顺利! - Jas

0

接口没有字段,因此当您使用IDisposable(或其他任何内容)时,您知道您只声明了您可以做什么。这似乎是它的主要意义。


但是字段应该被封装。 - Don Roby
@donroby:抱歉,我不太明白你的意思。你能解释一下吗?(不是封装是什么,我理解那个,而是它如何与我所说的相矛盾。)谢谢。 - Oren A
最好不需要知道某个东西是否有字段。我只是指出,我认为知道是否有字段并不是知道某个东西是否是接口的好理由。我可能会在实际答案中详细阐述... - Don Roby
像 donroby 一样,我认为这并没有回答为什么你会关心你的引用是类还是接口。 - Marcus Stade

0
区分接口和类可能是有用的,无论在 IDE 内部还是外部引用类型时,可以确定以下内容:
  • 我能否创建此类型的新实现?
  • 我能否在不支持多重继承的语言(例如 Java)中实现此接口?
  • 是否可以有多个此类型的实现?
  • 我能否在任意模拟框架中轻松模拟此接口?
值得注意的是,UML 区分接口和实现类。此外,“I” 前缀在三位作者 Booch、Jacobson 和 Rumbaugh 的《统一建模语言用户指南》示例中使用。(顺便提一下,这也说明仅仅通过 IDE 语法着色并不足以在所有情况下进行区分。)

你提到的例子在网上有吗?我很想看看! - Marcus Stade

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接