25

在用 Java 编程时,我几乎总是出于习惯,写这样的东西:

public List<String> foo() {
    return new ArrayList<String>();
}

大多数时候甚至没有考虑它。现在,问题是:我是否应该始终将接口指定为返回类型?还是建议使用接口的实际实现,如果可以,在什么情况下?

很明显,使用接口有很多优点(这就是它存在的原因)。在大多数情况下,库函数使用什么具体实现并不重要。但也许在某些情况下它确实很重要。例如,如果我知道我将主要随机访问列表中的数据,aLinkedList会很糟糕。但是如果我的库函数只返回接口,我根本不知道。为了安全起见,我什至可能需要将列表显式复制到ArrayList

List bar = foo();
List myList = bar instanceof LinkedList ? new ArrayList(bar) : bar;

但这看起来很可怕,我的同事可能会在自助餐厅私刑处死我。理所当然地。

你们有什么感想?你的指导方针是什么,你什么时候倾向于抽象解决方案,你什么时候透露你的实现细节以获得潜在的性能提升?

4

12 回答 12

32

返回适当的接口以隐藏实现细节。你的客户应该只关心你的对象提供了什么,而不是你如何实现它。如果您从私有 ArrayList 开始,然后决定其他更合适的方法(例如,LinkedLisk、跳过列表等),您可以在返回接口时更改实现而不影响客户端。返回具体类型的那一刻,机会就丢失了。

于 2009-05-31T17:08:17.130 回答
12

例如,如果我知道我将主要随机访问列表中的数据,那么 LinkedList 会很糟糕。但是如果我的库函数只返回接口,我根本不知道。为了安全起见,我什至可能需要将列表显式复制到 ArrayList。

正如其他人所提到的,您不必关心库如何实现功能,以减少耦合并提高库的可维护性。

如果您作为图书馆客户,可以证明实施对您的用例表现不佳,则可以联系负责人并讨论遵循的最佳路径(此案例的新方法或只是更改实施) .

也就是说,您的示例带有过早优化的味道。

如果该方法是或可能是关键的,它可能会在文档中提及实现细节。

于 2009-05-31T17:23:13.570 回答
11

由于无法用大量的 CS 引用来证明它的合理性(我是自学成才的),我在设计课程时总是遵循“接受最少派生,返回最多派生”的口头禅,这让我非常满意这些年。

我想这意味着就接口与具体返回而言,如果您试图减少依赖关系和/或解耦,返回接口通常更有用。但是,如果具体类实现的不仅仅是该接口,那么对于您的方法的调用者来说,获取具体类(即“最派生的”)通常更有用,而不是将它们任意限制为该返回对象功能的子集- 除非你真的需要来限制他们。再说一次,您也可以只增加界面的覆盖范围。像这样不必要的限制,我将其比作漫不经心的封课;你永远不会知道。只是谈谈该咒语的前一部分(对于其他读者),接受最小派生也为您的方法的调用者提供了最大的灵活性。

-Oisin

于 2009-05-31T17:05:12.093 回答
9

在OO编程中,我们希望尽可能多地封装数据。尽可能隐藏实际实现,尽可能高地抽象类型。

在这种情况下,我会回答只返回什么是有意义的。返回值是具体的类是否有意义?在你的例子中,问问自己:有人会在 foo 的返回值上使用 LinkedList 特定的方法吗?

  • 如果不是,只需使用更高级别的接口。它更加灵活,并且允许您更改后端
  • 如果是,问问自己:我不能重构我的代码以返回更高级别的接口吗?:)

您的代码越抽象,更改后端时所需的更改就越少。就这么简单。

另一方面,如果您最终将返回值强制转换为具体类,那么这是一个强烈的信号,表明您可能应该返回具体类。您的用户/队友不必了解或多或少的隐式合同:如果您需要使用具体方法,只需返回具体类,以清楚起见。

简而言之:代码抽象,但明确:)

于 2009-05-31T17:21:51.013 回答
8

很抱歉不同意,但我认为基本规则如下:

  • 对于输入参数,使用最通用的.
  • 对于输出值,最具体的.

因此,在这种情况下,您希望将实现声明为:

public ArrayList<String> foo() {
  return new ArrayList<String>();
}

原理:输入用例大家都知道,解释一下:使用接口,句号。但是,输出情况可能看起来违反直觉。您希望返回实现,因为您希望客户端获得有关接收内容的最多信息。在这种情况下,更多的知识就是更多的力量

示例 1:客户端想要获取第 5 个元素:

  • 返回集合:必须迭代到第 5 个元素与返回列表:
  • 返回列表:list.get(4)

示例 2:客户想要删除第 5 个元素:

  • return List:必须创建一个没有指定元素的新列表(list.remove()可选)。
  • 返回数组列表:arrayList.remove(4)

因此,使用接口很棒,这是一个很大的事实,因为它促进了可重用性,减少了耦合,提高了可维护性并让人们开心......但只有在用作输入时。

因此,同样,该规则可以表述为:

  • 对您提供的内容保持灵活。
  • 提供您提供的信息。

所以,下一次,请返回执行。

于 2014-07-11T20:01:32.513 回答
6

通常,对于 API 等面向公众的接口,返回接口(例如List)而不是具体实现(例如ArrayList)会更好。

使用ArrayListorLinkedList是该库的最常见用例应考虑的库的实现细节。当然,在内部,有private方法传递LinkedLists 不一定是一件坏事,如果它提供了使处理更容易的设施。

没有理由不应该在实现中使用具体类,除非有充分的理由相信List稍后会使用其他一些类。但是话又说回来,只要面向公众的部分设计得很好,更改实现细节就不应该那么痛苦。

图书馆本身对消费者来说应该是一个黑匣子,所以他们不必担心内部发生了什么。这也意味着应该设计该库,以便按照预期的方式使用它。

于 2009-05-31T17:09:12.933 回答
5

API 方法返回接口还是具体类并不重要。尽管这里的每个人都这么说,但一旦编写了代码,您几乎永远不会更改实现类。

更重要的是:始终为您的方法参数使用最小范围接口!这样,客户就拥有最大的自由,并且可以使用您的代码甚至不知道的类。

当一个 API 方法返回时ArrayList,我对此完全没有疑虑,但是当它需要一个ArrayList(或者,所有常见的,Vector)参数时,我考虑追捕程序员并伤害他,因为这意味着我不能使用Arrays.asList()Collections.singletonList()或者Collections.EMPTY_LIST.

于 2009-05-31T21:47:37.907 回答
4

通常,如果我在某个库的私有内部工作中,我只会传回内部实现,即使如此,也只是很少。对于所有公开且可能从模块外部调用的内容,我使用接口以及工厂模式。

以这种方式使用接口已被证明是编写可重用代码的一种非常可靠的方式。

于 2009-05-31T17:09:13.167 回答
4

主要问题已经得到解答,您应该始终使用该界面。然而,我只想评论

很明显,使用接口有很多优点(这就是它存在的原因)。在大多数情况下,库函数使用什么具体实现并不重要。但也许在某些情况下它确实很重要。例如,如果我知道我将主要随机访问列表中的数据,那么 LinkedList 会很糟糕。但是如果我的库函数只返回接口,我根本不知道。为了安全起见,我什至可能需要将列表显式复制到 ArrayList。

如果您要返回一个您知道随机访问性能较差的数据结构 - O(n) 并且通常是大量数据 - 您应该指定其他接口而不是 List,例如 Iterable,以便使用该库的任何人都会充分意识到只有顺序访问可用。

选择正确的返回类型不仅仅是接口与具体实现的关系,还与选择正确的接口有关。

于 2009-05-31T21:27:48.960 回答
1

您使用接口从实际实现中抽象出来。该接口基本上只是您的实现可以做什么的蓝图。

接口是好的设计,因为它们允许您更改实现细节而不必担心它的任何消费者会受到直接影响,只要您的实现仍然按照您的接口所说的那样做。

要使用接口,您可以像这样实例化它们:

IParser parser = new Parser();

现在 IParser 将成为您的接口,而 Parser 将成为您的实现。现在,当您使用上面的解析器对象时,您将针对接口 (IParser) 进行操作,而接口将针对您的实现 (Parser) 进行操作。

这意味着您可以随心所欲地更改 Parser 的内部工作,它永远不会影响与您的 IParser 解析器接口一起工作的代码。

于 2009-05-31T17:05:15.357 回答
1

如果您不需要具体类的功能,通常在所有情况下都使用接口。请注意,对于列表,Java 添加了一个RandomAccess标记类,主要是为了区分算法可能需要知道 get(i) 是否为常数时间的常见情况。

对于代码的使用,上面的 Michael 是对的,在方法参数中尽可能通用通常更为重要。在测试这种方法时尤其如此。

于 2009-06-02T20:44:44.517 回答
0

您会发现(或已经发现)当您返回接口时,它们会渗透到您的代码中。例如,您从方法 A 返回一个接口,然后您必须将一个接口传递给方法 B。

您正在做的是通过合同进行编程,尽管方式有限。

这为您提供了在幕后更改实现的巨大空间(前提是这些新对象满足现有合同/预期行为)。

鉴于所有这些,您在选择实现以及如何替代行为(包括测试 - 例如使用模拟)方面受益匪浅。如果您没有猜到,我完全赞成这一点,并尽可能减少(或引入)接口。

于 2009-05-31T17:12:10.137 回答