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

评论

请参阅developers.stackexchange.com/questions/150045/…

或使其更易于单元测试。

允许多个类实现接口,并使代码取决于接口,对于单元测试的隔离至关重要。如果要进行单元测试,则将有另一个类实现该接口。

关于此问题的Reddit讨论。

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

#1 楼

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

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

评论


+1测试意味着您几乎总是有两种实现

– jk。
2012年8月7日在8:16

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

–丹在火光中摆弄
2012年8月7日12:26

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

–丹在火光中摆弄
2012年8月7日17:11

我认为缺少接口对于YAGNI不是一个很好的例子,但是对于“破损的窗口”和缺少的文档而言。实际上,该类的用户被迫针对实现进行编码,而不是按需进行抽象。

–法比奥·弗拉卡西(Fabio Fracassi)
2012年8月8日在9:02

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

–埃里克·雷彭(Erik Reppen)
13年2月12日,0:20



#2 楼

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

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


如果如果使Foo类直接引用BarImpl类,则您将坚定地承诺每次更改Foo时都要更改BarImpl。基本上,您将它们视为分成两个类的一个代码单元。
如果使Foo引用接口Bar,那么您将致力于避免更改Foo时更改BarImpl

如果在应用程序的关键点定义接口,则需要仔细考虑它们应该支持和不应该支持的方法,并清楚地注释接口以描述实现应如何表现(以及如何),您的应用程序将更容易理解,因为这些带注释的界面将提供某种形式的应用程序规范,即对应用程序行为的描述。这样可以更轻松地阅读代码(而不是询问“此代码到底要做什么”,您可以问“此代码如何完成其​​应该做什么”)。

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

评论


如果可以的话,我将不止一次投票。海事组织对这个问题的最佳答案。

–法比奥·弗拉卡西(Fabio Fracassi)
2012年8月8日在9:05

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

–马修·芬利(Matthew Finlay)
2012年8月8日23:50

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

– sacundim
2012年8月9日,0:56

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

– sacundim
2012年8月9日在1:01

@sacundim如果将Foo类直接引用到BarImpl类,则每次更改BarImpl时都坚决承诺要更改Foo。更改BarImpl时,使用接口Bar在Foo中可以避免哪些更改?由于只要BarImpl中方法的签名和功能不变,即使没有接口(接口的全部用途),Foo也不需要更改。我说的是只有一个类的情况,即BarImpl实现Bar。对于多类方案,我了解依赖倒置原理及其接口的帮助。

– Shishir Gupta
18年6月18日在18:05



#3 楼

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

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

评论


并非总是如此,如果该类是泛型类,则可以在一个类中使用接口。

– John Isaiah Carmona
2012年8月8日,下午5:29

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

–汉斯·彼得·斯托尔
2012年8月13日在18:57



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

–达雷尔·蒂格(Darrell Teague)
16 Mar 23 '16 at 14:27

#4 楼

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

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

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

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

评论


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

–装饰
2012年8月7日在8:21

@Deco:好的模拟框架(包括JMock)甚至不需要接口。

–迈克尔·伯格沃德(Michael Borgwardt)
2012年8月7日在22:45

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

– Alb
2012年8月8日13:41



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

– Avner Shahar-Kashtan
2012年8月8日13:47

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

–埃里克·雷彭(Erik Reppen)
13年2月12日,0:39

#5 楼

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

为所有内容制作Foo / FooImpl确实要花钱。 IDE可以免费创建接口/实现,但是在浏览代码时,foo.doSomething()上的F3 / F12会带来额外的认知负载,从而带您到接口签名,而不是所需的实际实现。另外,您有两个文件,而不是所有文件都杂乱无章。

因此,仅在实际需要某些文件时才应该这样做。

现在解决反对论点:

我需要依赖关系注入框架的接口

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

我需要模拟进行单元测试

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

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

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

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

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

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

另请参见:


http://www.adam-bien.com/roller/abien/entry/service_s_new_serviceimpl
http:// martinfowler.com/bliki/InterfaceImplementationPair.html
https://blog.8thlight.com/uncle-bob/2014/05/10/WhenToMock.html
http://wrschneider.github.io/ 2015/07/27 / foo-fooimpl-pairs.html


评论


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

–法律
15年8月1日在8:53

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

–wrschneider
15年8月3日,11:32

@Jules您可以在Java中模拟一个具体的类,包括其构造函数。

–叙利亚
15年11月19日在15:03

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

–法律
15年11月19日在17:54

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

–叙利亚
15年11月19日在18:22



#6 楼

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


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


正如我在回答Yanni的回答中所指出的那样,我认为您不可能对接口有一个严格的规定。根据定义,该规则必须灵活。我对接口的规则是,在创建API的任何地方都应使用接口。而且,应该在跨越一个责任域到另一个责任域的任何地方创建一个API。对于(一个非常人为的)示例,假设您正在构建一个Car类。在您的课程中,您肯定需要一个UI层。在此特定示例中,采用IginitionSwitchSteeringWheelGearShiftGasPedalBrakePedal的形式。由于这辆车包含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。在肉类汽车中,点火开关负责:


验证(未识别)用户(他们需要正确的钥匙)
向启动器提供电流实际启动汽车
向点火系统提供电流,以便它可以继续运行
切断电流,使汽车停止。
取决于您的看法,大多数(全部)较新的汽车有一个开关,可以防止在变速器不在驻车挡位时从点火开关上拔下钥匙,因此这可能是其范畴的一部分。 (实际上,这意味着我需要重新考虑和重新设计系统...)

这些属性构成了IgnitionSwitch的领域,或者换句话说,它了解并负责。 />
IgnitionSwitchGasPedal不承担任何责任。点火开关在任何方面都完全不了解油门踏板。它们都彼此完全独立地运行(尽管没有它们,一辆汽车将毫无价值!)。

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

评论


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

– Lamin Sanneh
2012年8月8日在11:50



@LaminSanneh,详细说明。有帮助吗?

–Wayne Werner
2012年8月8日在20:18

#7 楼

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

#8 楼

来自MSDN:

接口更适合于您的应用程序需要许多可能不相关的对象类型来提供某些功能的情况。
接口比基类更灵活,因为您可以定义单个实现可以实现多个接口。
在不需要从基类继承实现的情况下,接口更好。
在不能使用类继承的情况下,接口非常有用。例如,结构不能从类继承,但是它们可以实现接口。

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

#9 楼

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

该类实现了另一个我不想公开的接口
桥接第三方代码的适配器类经常发生。

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;
}


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

#10 楼

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

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

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

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

#11 楼

自从您问了这个问题之后,我想您已经看到了隐藏多个实现的接口的好处。这可以通过依赖关系反转原理来体现。

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

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

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

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

评论


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

–负责人
2014年3月21日在17:25

#12 楼

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

创建接口的过程几乎一无所有,因此很容易以“切碎的意大利面条”代码结尾。我尊重阿扬德·拉希恩(Ayende Rahien)的大智慧,他在这个问题上发表了一些非常明智的话:

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

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

评论


'切碎的意大利面条'代码也被称为馄饨代码c2.com/cgi/wiki?

–窗帘狗
2012年8月7日在20:38

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

– dodgy_coder
2012年8月8日在1:53

#13 楼

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

#14 楼

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

#15 楼

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

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

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

所以我将只为更高级别的类定义接口,其中您需要实现的抽象。

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