当只有一个类会实现该接口时,我需要使用该接口吗?


242

接口的整个观点不是多个类都遵循一组规则和实现吗?



16
或使其更易于单元测试。

11
允许多个类实现接口并让您的代码依赖于接口对于单元测试的隔离至关重要。如果要进行单元测试,则将有另一个类实现该接口。
StuperUser 2012年

2
关于这个问题的讨论
扬尼斯2012年

6
公共领域和方法本身就是一个“接口”。如果有计划地缺乏多态性,则没有理由使用接口。其他提到的单元测试是计划使用多态性。
mike30 2012年

Answers:


204

严格来说,不,您可以,YAGNI适用。就是说,您花费在创建界面上的时间是最少的,尤其是如果您有一个方便的代码生成工具为您完成大部分工作时。如果您不确定是否需要该接口,我会说最好是支持接口的定义。

此外,即使对于单个类也使用接口,将为您提供单元测试的另一种模拟实现,而不是在生产中。在这一点上,Avner Shahar-Kashtan的答案有所扩展。


84
+1测试意味着你几乎总是有两种实现反正
JK。

24
@YannisRizos由于Yagni,不同意您的后者观点。事实上,从类的公共方法启动接口很简单,在消费类中用IFoo替换CFoo也是如此。没有必要事先编写它是没有意义的。
丹·尼利

5
我仍然不确定我是否遵循您的推理。由于代码生成工具使添加该事实变得更加便宜,因此,在您明确需要该接口之前,我几乎没有理由创建该接口。
丹·尼利

6
我认为缺少接口对于YAGNI来说不是一个很好的例子,但对于“破损的窗口”和缺少的文档来说,这不是一个好例子。实际上,该类的用户被迫针对实现进行编码,而不是按需进行抽象。
法比奥·弗拉卡西

10
为什么您会为了满足某些测试框架而无意义地污染代码库?我的意思是说真的,我只是一个自学成才的JavaScript客户端专家,试图弄清WTF在C#和Java开发人员OOD实现方面是错误的,我在追求成为一个更加全面的通才时经常遇到,但是为什么不这样做呢?他们会把IDE丢在手里吗,直到他们学会了如何在大学学习做这种事情时,才让您收回IDE,直到您学习如何编写清晰易懂的代码为止?那真是淫秽。
Erik Reppen

144

我会回答,是否需要一个接口并不取决于实现该接口的类数。接口是用于定义应用程序的多个子系统之间的合同的工具。因此真正重要的是如何将应用程序划分为子系统。无论有多少类实现它们,都应该有接口作为封装子系统的前端。

这是一个非常有用的经验法则:

  • 如果让班级Foo直接指向班级BarImpl,则Foo每次更改时都坚决要求更改BarImpl。您基本上将它们视为分成两个类的一个代码单元。
  • 如果Foo引用界面 Bar,则是在承诺自己,以避免更改Foo时进行更改BarImpl

如果您在应用程序的关键点上定义了接口,则需要仔细考虑它们应该支持和不应该支持的方法,并清楚地注释接口,以描述实现的行为方式(以及如何实现),你的应用将是一个更容易理解,因为这些评论接口将提供某种规范的它是如何在应用程序的描述的意图行事。这使阅读代码变得更加容易(而不是询问“此代码应该做什么”,您可以问“该代码如何完成应做的事情”)。

除了所有这些(或者实际上是因为它)之外,接口还促进了单独的编译。由于接口要比其实现更容易编译并且具有更少的依赖关系,这意味着如果编写类Foo以使用接口Bar,则通常可以重新编译BarImpl而无需重新编译Foo。在大型应用程序中,这可以节省大量时间。


14
如果可以的话,我将不止一次投票。海事组织对这个问题的最佳答案。
法比奥·弗拉卡西

4
如果该类仅扮演一个角色(即仅为其定义一个接口),那么为什么不通过公共/私有方法来做到这一点呢?
马修·芬利

4
1.代码组织;在自己的仅具有签名和文档注释的文件中拥有接口有助于保持代码的清洁。2.迫使您公开自己不想公开的方法的框架(例如,通过公共设置器注入依赖项的容器)。3.如前所述,单独编译;如果类Foo取决于接口,BarBarImpl无需重新编译就可以对其进行修改Foo。4.比公共/私有产品提供更细粒度的访问控制(将相同的类通过不同的接口提供给两个客户端)。
sacundim 2012年

3
最后一个:5.客户端(理想情况下)不必理会我的模块有多少个类,或者有多少个甚至不知道。他们应该看到的只是一组类型,还有一些工厂或立面可以使球滚动。即,即使封装您的库包含哪些类,我也经常看到有价值的东西。
sacundim 2012年

3
@sacundim If you make class Foo refer directly to class BarImpl, you're strongly committing yourself to change Foo every time you change BarImpl在更改BarImpl时,使用Foo可以避免哪些更改interface Bar?由于只要BarImpl中方法的签名和功能不变,即使没有接口(接口的全部用途),Foo也不需要更改。我说的是只有一个类即BarImpl实现Bar 的场景。对于多类方案,我了解依赖倒置原理及其接口的帮助。
Shishir Gupta

101

指定接口以定义行为,即一组功能/方法的原型。实现接口的类型将实现该行为,因此当您处理此类类型时,您会(部分地)知道其具有的行为。

如果您知道接口定义的行为将仅使用一次,则无需定义接口。(保持简单,愚蠢)


并非总是如此,如果该类是泛型类,则可以在一个类中使用接口。
约翰·伊赛亚·卡莫纳

此外,您可以在现代IDE中最终需要时通过“提取接口”重构轻松地引入接口。
汉斯·彼得·斯托尔

考虑一个只有公共静态方法和私有构造函数的实用程序类,这些类完全被Joshua Bloch等人的作者接受和推广。
Darrell Teague

63

从理论上讲,您不应该仅仅为了拥有一个接口而拥有一个接口,Yannis Rizos的答案暗示了进一步的复杂性:

当您编写单元测试并使用Moq或FakeItEasy之类的模拟框架(以命名我最近使用的两个框架)时,您隐式地创建了另一个实现该接口的类。搜索代码或进行静态分析可能会声称只有一个实现,但实际上存在内部模拟实现。每当您开始编写模拟时,您都会发现提取接口是有意义的。

但是等等,还有更多。在更多情况下,存在隐式接口实现。例如,使用.NET的WCF通信堆栈,可以生成远程服务的代理,该代理又可以实现该接口。

在干净的代码环境中,我同意此处的其余答案。但是,请注意可能使用接口的任何框架,模式或依赖项。


2
+1:我不确定YAGNI是否适用于此,因为具有接口并使用利用接口的框架(例如JMock等)实际上可以节省您的时间。
装饰

4
@Deco:好的模拟框架(其中的JMock甚至不需要接口)。
Michael Borgwardt

12
仅由于您的模拟框架的限制而创建接口对我来说似乎是一个可怕的原因。无论如何,使用EasyMock模拟类就像界面一样容易。
Alb 2012年

8
我会说相反。根据定义,在测试中使用模拟对象意味着创建接口的替代实现。无论您创建自己的FakeImplementation类还是让模拟框架为您完成繁重的工作,都是如此。可能会有一些框架,例如EasyMock,使用各种技巧和低级技巧来模拟具体的类-并赋予它们更多的功能!-但从概念上讲,模拟对象是合同的替代实现。
Avner Shahar-Kashtan'8年

1
您不会取消生产中的测试。为什么对于不需要接口的类的模拟需要接口?
Erik Reppen

32

不,您不需要它们,我认为为每个类引用自动创建接口是一种反模式。

为所有内容制作Foo / FooImpl确实要付出代价。IDE可以免费创建接口/实现,但是在浏览代码时,F3 / F12带来了额外的认知负担,foo.doSomething()使您无法使用接口签名,而没有想要的实际实现。另外,您有两个文件,而不是一个文件。

因此,只有在确实需要某些东西时才应该这样做。

现在解决反对意见:

我需要用于依赖注入框架的接口

支持框架的接口是遗留的。在Java中,接口以前是CGLIB之前的动态代理的要求。今天,您通常不需要它。它被认为是进步,对开发人员来说是福音,对您来说,EJB3,Spring等不再需要它们。

我需要模拟进行单元测试

如果您编写自己的模拟并具有两个实际的实现,那么一个接口是合适的。如果您的代码库同时具有FooImpl和TestFoo,那么我们可能不会首先进行讨论。

但是,如果您使用的是Moq,EasyMock或Mockito之类的模拟框架,则可以模拟类,并且不需要接口。这类似于foo.method = mockImplementation在动态语言中设置可以分配方法的位置。

我们需要接口来遵循依赖倒置原则(DIP)

DIP表示您构建时要依赖合同(接口)而不是实现。但是,一个类已经是一个契约和一个抽象。这就是public / private关键字的用途。在大学里,规范的例子就像是Matrix或Polynomial类-消费者拥有一个公共API来创建矩阵,添加矩阵等,但是不允许关心矩阵是稀疏形式还是稠密形式。不需要IMatrix或MatrixImpl即可证明这一点。

同样,DIP通常在每个类/方法调用级别上都过度应用,而不仅是在主要模块边界上。您过度使用DIP的迹象是您的界面和实现以锁定步长进行更改,因此您必须触摸两个文件才能进行更改,而不是一个。如果正确应用了DIP,则意味着您的界面不必经常更改。另外,另一个迹象是您的界面只有一个真正的使用者(它自己的应用程序)。如果您要构建用于在许多不同应用中使用的类库,则情况会不同。

这是鲍勃·马丁叔叔关于模拟的观点的必然结果-您只需要在主要架构边界进行模拟即可。在Webapp中,HTTP和DB访问是主要的界限。两者之间的所有类/方法调用都不是。DIP也是如此。

也可以看看:


4
不应考虑使用模拟类而不是接口(至少在Java和C#中),因为无法阻止超类构造函数运行,这可能会导致模拟对象以意外方式与环境交互。模拟接口更安全,更轻松,因为您不必考虑构造函数代码。
2015年

4
我没有遇到模拟类的问题,但是我对IDE导航无法按预期工作感到沮丧。解决一个实际问题胜过一个假设的问题。
wrschneider 2015年

1
@Jules您可以在Java中模拟具体的类,包括其构造函数。
assylias 2015年

1
@assylias如何阻止构造函数运行?
2015年

2
@Jules这取决于您的模拟框架-例如,使用jmockit,您可以编写new Mockup<YourClass>() {}并且整个类(包括其构造函数)都将被模拟,无论是接口,抽象类还是具体类。如果愿意,还可以“重写”构造函数的行为。我想在Mockito或Powermock中有等效的方法。
assylias,2015年

22

看起来栅栏两侧的答案都可以总结为:

设计良好,并将接口放在需要接口的地方。

正如我在对Yanni的回答中所指出的那样,我认为您不可能对接口有一个严格的规定。根据定义,该规则必须灵活。我对接口的规则是,在创建API的任何地方都应使用接口。而且,您应该在跨界从一个责任域到另一个责任域的任何地方创建一个API。

对于(一个非常糟糕的)示例,假设您正在构建一个Car类。在您的课程中,您肯定需要一个UI层。在这个特定的例子中,它需要的形式IginitionSwitchSteeringWheelGearShiftGasPedal,和BrakePedal。由于这辆车包含一个AutomaticTransmission,你不需要一个ClutchPedal。(由于这是一辆糟糕的汽车,因此没有空调,收音机或座椅。事实上,地板也缺失了-您只需要挂在那个方向盘上,并希望最好就可以了!)

那么这些类中的哪些需要接口?答案可能是全部,也可能全都不是,具体取决于您的设计。

您可能会有一个如下所示的界面:

Interface ICabin
    Event IgnitionSwitchTurnedOn()
    Event IgnitionSwitchTurnedOff()
    Event BrakePedalPositionChanged(int percent)
    Event GasPedalPositionChanged(int percent)
    Event GearShiftGearChanged(int gearNum)
    Event SteeringWheelTurned(float degree)
End Interface

届时,这些类的行为将成为ICabin接口/ API的一部分。在此示例中,类(如果有的话)可能很简单,具有一些属性以及一个或两个函数。您在设计中隐含地指出的是,这些类仅存在以支持您所拥有的ICabin的任何具体实现,它们不能独立存在,或者在ICabin上下文之外毫无意义。

这是您不对私有成员进行单元测试的原因-它们用于支持公共API,因此应通过测试API来测试其行为。

因此,如果您的班级仅是为了支持另一个班级而存在,并且从概念上讲您认为该班级并不真正拥有其自己的域,那么可以跳过该界面。但是,如果您的课程非常重要,以至于您认为它的成长足以拥有自己的领域,那么请继续给它一个接口。


编辑:

经常(包括在此答案中)您会读到“域”,“依赖项”(经常与“注入”结合)之类的东西,这些东西在您开始编程时对您并不重要(他们肯定没有对我来说毫无意义)。对于域,它的含义完全是这样:

行使统治或权威的领土;拥有主权或联邦之类的财产。也用于比喻。[WordNet感2] [1913 Webster]

就我的示例而言-让我们考虑IgnitionSwitch。在肉类车中,点火开关负责:

  1. 验证(不识别)用户(他们需要正确的密钥)
  2. 向启动器提供电流,以便它实际上可以启动汽车
  3. 向点火系统提供电流,使其可以继续运行
  4. 关闭电流,使汽车停止。
  5. 根据您的看法,在大多数(所有?)较新的汽车中,都有一个开关,可防止在变速器不在Park时从点火开关上拔下钥匙,因此这可能是其范畴的一部分。(实际上,这意味着我需要重新考虑和重新设计系统...)

这些属性构成了IgnitionSwitch,或者换句话说,它知道并负责。

IgnitionSwitch是不负责的GasPedal。点火开关在任何方面都完全不了解油门踏板。它们都彼此完全独立地运行(尽管没有它们,一辆汽车将毫无价值!)。

如我最初所说,这取决于您的设计。您可以设计一个IgnitionSwitch具有两个值的:开(True)和关(False)。或者,您可以设计它以验证为其提供的密钥以及许多其他操作。这是成为开发人员要决定在哪里划清界限的难点-坦白说,大多数情况下,这是完全相对的。但是,这些内容很重要-那是您的API所在的地方,因此也应该是您界面的所在。


您能否详细说明“拥有它自己的域”是什么意思?
拉明·桑尼

1
@LaminSanneh,详细说明。有帮助吗?
韦恩·维尔纳

8

(YAGNI),除非您打算使用此接口编写其他类的测试,并且这些测试将从模拟接口中受益。


8

MSDN

接口更适合于您的应用程序需要许多可能不相关的对象类型来提供某些功能的情况。

接口比基类更灵活,因为您可以定义一个可以实现多个接口的实现。

在不需要从基类继承实现的情况下,接口会更好。

在无法使用类继承的情况下,接口很有用。例如,结构不能从类继承,但是它们可以实现接口。

通常,在单个类的情况下,不需要实现接口,但是考虑到项目的未来,正式定义类的必要行为可能会很有用。


5

要回答这个问题:不仅如此。

接口的一个重要方面是意图。

接口是“不包含数据但公开行为的抽象类型”- 接口(计算)因此,如果这是类支持的一种行为或一组行为,则接口很可能是正确的模式。但是,如果行为是类所体现的概念所固有的,那么您可能根本就不需要接口。

要问的第一个问题是您要表示的事物或过程的本质是什么。然后继续以给定方式实现这种性质的实际原因。


5

自从您问了这个问题以来,我想您已经看到了隐藏多个实现的接口的好处。这可以通过依赖倒置原理来证明。

但是,是否需要一个接口并不取决于其实现的数量。接口的真正作用是定义一个合同,该合同说明应提供什么服务而不是应如何实现。

合同确定后,两个或多个团队可以独立工作。假设您正在处理模块A,并且它依赖于模块B,那么在B上创建接口的事实使您可以继续工作而不必担心B的实现,因为所有细节都被接口隐藏了。因此,分布式编程成为可能。

即使模块B仅具有其接口的一种实现,该接口仍然是必需的。

总之,界面向用户隐藏了实现细节。对接口进行编程有助于编写更多文档,因为必须定义合同,编写更多模块化软件,促进单元测试并加快开发速度。


2
每个类都可以具有一个公共接口(公共方法)和一个私有接口(实现细节)。我可以说阶级的公共接口就是契约。您不需要额外的元素即可执行该合同。
Fuhrmanator 2014年

4

这里的所有答案都很好。确实,大多数时候您不需要实现其他接口。但是在某些情况下,您可能仍要这样做。在某些情况下,我会这样做:

该类实现了另一个我不希望通过
桥接第三方代码的适配器类经常发生的接口。

interface NameChangeListener { // Implemented by a lot of people
    void nameChanged(String name); 
} 

interface NameChangeCount { // Only implemented by my class
    int getCount();
}

class NameChangeCounter implements NameChangeListener, NameChangeCount {
    ...
}

class SomeUserInterface {
    private NameChangeCount currentCount; // Will never know that you can change the counter
}

该类使用一种特定的技术,该技术
通常不会与外部库进行交互。即使只有一个实现,我也会使用一个接口来确保不会与外部库引入不必要的耦合。

interface SomeRepository { // Guarantee that the external library details won't leak trough
    ...
}

class OracleSomeRepository implements SomeRepository { 
    ... // Oracle prefix allow us to quickly know what is going on in this class
}

跨层通信
即使只有一个UI类曾经实现一个域类,它也可以在这些层之间实现更好的分隔,最重要的是,它避免了循环依赖性。

package project.domain;

interface UserRequestSource {
    public UserRequest getLastRequest();
}

class UserBehaviorAnalyser {
    private UserRequestSource requestSource;
}

package project.ui;

class OrderCompleteDialog extends SomeUIClass implements project.domain.UserRequestSource {
    // UI concern, no need for my domain object to know about this method.
    public void displayLabelInErrorMode(); 

    // They most certainly need to know about *that* though
    public UserRequest getLastRequest();
}

大多数对象只能使用该方法的一个子集
大多数情况是在具体类上有一些配置方法时发生的

interface Sender {
    void sendMessage(Message message)
}

class PacketSender implements Sender {
    void sendMessage(Message message);
    void setPacketSize(int sizeInByte);
}

class Throttler { // This class need to have full access to the object
    private PacketSender sender;

    public useLowNetworkUsageMode() {
        sender.setPacketSize(LOW_PACKET_SIZE);
        sender.sendMessage(new NotifyLowNetworkUsageMessage());

        ... // Other details
    }
}

class MailOrder { // Not this one though
    private Sender sender;
}

因此,最后我使用接口的原因与使用私有字段的原因相同:其他对象不应访问他们不应访问的内容。如果遇到这种情况,即使只有一个类实现该接口,我也会引入一个接口。


2

接口确实很重要,但是请尝试控制所拥有的接口数量。

在为几乎所有内容创建接口的过程中,最终很容易获得“切碎的意大利面条”代码。我顺应Ayende Rahien的更大智慧,他在这个问题上发表了一些非常明智的话:

http://ayende.com/blog/153889/limit-your-abstractions-analyzing-a-ddd-application

这是他整个系列的第一篇文章,所以请继续阅读!


“切碎的意大利面条”代码也称为馄饨代码c2.com/cgi/wiki?RavioliCode
CurtainDog 2012年

在我看来,听起来更像千层面代码或果仁蜜饼代码-层数太多。;-)
dodgy_coder 2012年

2

在这种情况下,您仍可能要引入接口的一个原因是要遵循“ 依赖倒置原则”。也就是说,使用该类的模块将取决于它的抽象(即接口),而不取决于具体的实现。它将高级组件与低级组件分离。


2

没有任何真正的理由要做任何事情。接口是为了帮助您而不是输出程序。因此,即使该接口由一百万个类实现,也没有规则说您必须创建一个。您可以创建一个代码,以便当您或其他使用您代码的人想要更改时,可以渗入所有实现中。创建接口将在以后所有可能需要创建另一个实现该接口的类的情况下为您提供帮助。


1

并非总是需要为类定义接口。

像值对象之类的简单对象没有多种实现。他们也不需要被嘲笑。可以单独测试实现,并且在测试依赖它们的其他类时,可以使用实际值对象。

请记住,创建接口是有代价的。它需要在实现过程中进行更新,它需要一个额外的文件,并且某些IDE在缩放实现时(而不是在界面上)会遇到问题。

因此,我只为需要从实现中抽象的高级类定义接口。

请注意,使用类可以免费获得接口。除了实现之外,一个类还根据一组公共方法定义一个接口。该接口由所有派生类实现。它并不是严格意义上的接口,但是可以完全相同的方式使用。因此,我认为没有必要重新创建以类名存在的接口。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.