在与基类相同的文件中声明接口,这是一种好习惯吗?


29

为了可互换和可测试,通常具有逻辑的服务需要具有接口,例如

public class FooService: IFooService 
{ ... }

在设计方面,我同意这一点,但是困扰我这种方法的一件事是,对于一项服务,您需要声明两件事(类和接口),而在我们的团队中,通常需要声明两个文件(一个用于类,一个用于接口)。另一个不适之处是导航困难,因为在IDE(VS2010)中使用“转到定义”将指向该接口(因为其他类均引用该接口),而不是实际的类。

我以为将IFooService与FooService放在同一文件中会减少上述怪异现象。毕竟,IFooService和FooService非常相关。这是一个好习惯吗?IFooService必须位于其自己的文件中有充分的理由吗?


3
如果您只有一种特定接口的实现,则对该接口没有真正的逻辑需求。为什么不直接使用该类?
Joris Timmermans 2012年

11
@MadKeithV是否具有松散耦合和可测试性?
Louis Rhys 2012年

10
@MadKeithV:你是正确的。但是,当您为依赖IFooService的代码编写单元测试时,通常会提供MockFooService,这该接口的第二个实现。
Doc Brown

5
@DocBrown-如果您有多个实现,则注释显然消失了,但是能够直接转到“单个实现”的实用程序也就消失了(因为至少有两个)。然后问题就变成了:在具体实现中,在使用接口时,接口描述/文档中不应该包含哪些信息?
Joris Timmermans 2012年

Answers:


24

它不必保存在自己的文件中,但是您的团队应该决定一个标准并坚持下去。

同样,“转到定义”将您带到该接口是正确的,但是如果您安装了Resharper,只需单击一下即可打开该接口的派生类/接口列表,所以这没什么大不了的。这就是为什么我将接口保存在单独的文件中。

 


5
我同意这个答案,毕竟IDE应该适应我们的设计,而不是反过来,对吧?
marktani 2012年

1
另外,如果没有Resharper,F12和Shift + F12几乎可以做同样的事情。甚至没有右键单击:)(公平地说,它只会带您直接使用该界面,不确定Resharper版本的用途)
Daniel B

@DanielB:在Resharper中,Alt-End转到实现(或为您提供可能实现的列表)。
StriplingWarrior 2012年

@StriplingWarrior然后看起来也正是香草VS所做的。F12 =转到定义,Shift + F12 =转到实现(我很确定这在没有Resharper的情况下仍然有效,尽管我确实安装了它,所以很难确认)。这是最有用的导航快捷方式之一,我只是在传播。
Daniel B

@DanielB:Shift-F12的默认值为“查找用法”。jetbrains.com/resharper/webhelp/…–
StriplingWarrior

15

我认为您应该将它们分开存放。如您所说,其想法是保持可测试性和可互换性。通过将接口与实现放在同一个文件中,可以将接口与特定实现相关联。如果您决定创建模拟对象或其他实现,则该接口在逻辑上不会与FooService分开。


10

根据SOLID,不仅您应该创建接口,而且不仅应在不同的文件中,而且还应在不同的程序集中。

为什么?因为对源文件的任何更改都必须重新编译该程序集,而对源文件的任何更改都需要重新编译该程序集,而对程序集的任何更改都需要重新编译任何从属程序集。因此,如果您基于SOLID的目标是能够用实现B替换实现A,而依赖于接口I的类C不必知道区别,则必须确保使用I进行组装它不会改变,从而保护了用法。

“但这只是重新编译”,我听到您提出抗议。可能是这样,但是在您的智能手机应用程序中,这更容易满足用户的数据带宽;是下载一个已更改的二进制文件,还是使用依赖于该代码的代码下载该二进制文件和其他五个二进制文件?并非所有程序都被编写为供LAN上的台式计算机使用。即使在那种带宽和内存便宜的情况下,较小的补丁程序发行版也很有价值,因为通过Active Directory或类似的域管理层将它们发布到整个LAN并不容易。您的用户只需等待几秒钟即可在下次登录时应用该功能,而无需等待几分钟即可重新安装整个程序。更不用说,构建项目时必须重新编译的程序集越少,构建速度就越快,

现在,免责声明:这并非总是可行或可行的。最简单的方法是创建一个集中的“接口”项目。这有其缺点。代码变得更不可重用了,因为必须在其他应用中重用应用程序的持久层或其他关键组件来引用接口项目和实施项目。您可以通过将接口拆分为更紧密耦合的程序集来解决该问题,但随后您的应用程序中会有更多项目,这会使完整版本非常痛苦。关键是保持平衡,并保持松耦合的设计;您通常可以根据需要移动文件,因此当您看到一个类将需要进行很多更改,或者需要定期执行该接口的新实现时(例如与其他软件的新支持版本进行接口,


8

为什么使用单独的文件会感到不适?对我来说,它更加整洁和清洁。如果要在解决方案资源管理器中查看较少的文件,通常会创建一个名为“ Interfaces”的子文件夹并将其IFooServer.cs文件粘贴在那里。

接口在其自己的文件中定义的原因与类通常在其自己的文件中定义的原因相同:当逻辑结构和文件结构相同时,项目管理会更简单,因此您始终知道已定义给定类的文件在调试时(异常堆栈跟踪通常会为您提供文件和行号)或在源代码控制存储库中合并源代码时,这可以使您的工作变得更轻松。


6

通常,让您的代码文件仅包含单个类或单个接口是一个好习惯。但是,这些编码实践是达到目的的一种手段-更好地组织代码,使其更易于使用。如果您和您的团队发现将类及其接口保持在一起,则使用起来会更加容易。

就个人而言,我更喜欢在只有一个实现接口的类(例如您的情况)下,将接口和类放在同一文件中。

关于导航方面的问题,我强烈建议ReSharper。它包含一些非常有用的快捷方式,用于直接跳转到实现特定接口方法的方法。


3
+1表示团队的做法应决定文件的结构,并采用违反规范的方法。

3

很多时候,您希望您的接口不仅在类的单独文件中,而且甚至在单独的程序集中。

例如,如果您可以控制电线的两端,则客户端和服务都可以共享 WCF服务合同接口。通过将接口移动到其自己的程序集中,它将具有更少的程序集相关性。这使得客户端使用起来更加容易,从而放松了其实现的耦合。


2

如果有接口1的单一实现,则很少(如果有的话)有意义。如果将公共接口和实现该接口的公共类放在同一文件中,则很有可能不需要接口。

当您与该接口共同定位的类是abstract,并且您知道该接口的所有实现都应该继承该抽象类时,将二者定位在同一文件中是有意义的。您仍然应该仔细考虑使用接口的决定:问问自己抽象类本身是否合适,如果答案是肯定的,则放弃该接口。

通常,尽管如此,您应该坚持“一个公共类/接口对应一个文件”的策略:易于遵循,并且使源代码树更易于导航。


1一个值得注意的例外是,当您需要具有用于测试目的的接口时,因为您选择的模拟框架对您的代码提出了这一额外要求。


3
实际上,在.NET中为一个类提供一个接口是一种非常正常的模式,因为它允许单元测试用模拟,存根,间谍或其他测试双精度替代依赖项。
皮特

2
@Pete我编辑了答案,将其作为注释。我不会将此模式称为“正常”模式,因为它会使您的可测试性问题“泄漏”到您的主代码库中。现代的模拟框架可在很大程度上帮助您克服此问题,但是在那儿拥有接口无疑会大大简化事情。
dasblinkenlight 2012年

2
@KeithS仅当确定不能对其进行子类化并确定该类时,才应在设计中使用没有接口的类sealed。如果您怀疑将来可能会添加第二个子类,则可以立即放入一个接口。PS一个体面质量的重构助手可以以单个ReSharper许可证的适中价格提供给您:)
dasblinkenlight 2012年

1
@KeithS是的,所有不是专门为子类设计的类都应该密封。实际上,wish类在默认情况下是密封的,从而迫使您显式地指定要继承的类,就像该语言当前通过标记它们来迫使您指定要覆盖的函数一样virtual
dasblinkenlight 2012年

2
@KeithS:当您的一个实现变成两个时,会发生什么?与实现更改时相同;您更改代码以反映该更改。YAGNI在这里适用。您永远不会知道将来会发生什么变化,所以做最简单的事情就可以了。(不幸的是,这个准则使测试
变得

2

接口属于客户,而不属于其实现,如Robert C. Martin所定义的敏捷原则,实践和模式。因此,在同一位置结合接口和实现是违反其原理的。

客户端代码取决于接口。这样就可以在没有实现的情况下编译和部署客户端代码和接口。比起您可以有不同的实现方式,它们就像客户端代码的插件一样。

更新:这不是仅敏捷的原则。早在94年的“四大设计模式”就已经在谈论客户坚持使用接口以及对接口进行编程。他们的观点是相似的。


1
接口的使用远远超出了敏捷拥有的领域。敏捷是一种开发方法,它以特定方式使用语言构造。接口是一种语言构造。

1
是的,但是无论我们谈论的是敏捷与否,接口仍然属于客户端而不是实现。
Patkos Csaba 2012年

我在这方面和你在一起。
Marjan Venema 2012年

0

我个人不喜欢界面前的“ I”。作为这种类型的用户,我不想看到这是否是一个界面。类型是有趣的事情。在依赖性方面,您的FooService是IFooService的一种可能的实现。接口应靠近使用场所。另一方面,该实现应该在易于更改而又不影响客户的地方。因此,我通常希望使用两个文件。


2
作为用户,我想知道一个类型是否是一个接口,因为例如它意味着我不能使用new它,但是另一方面,我可以使用它的协变量或反变量。而且I,.Net是.Net中已建立的命名约定,因此,即使只是为了与其他类型保持一致,也有理由遵守它。
svick 2012年

Iin接口开始只是我关于两个文件分离的论点的介绍。该代码可读性更好,并且使依赖关系的反转更清晰。
ollins 2012年

4
不管喜欢与否,在.NET中在接口名称前使用'I'是事实上的标准。在我看来,不遵循.NET项目中的标准将违反“最小惊讶原则”。
皮特2012年

我的主要观点是:分为两个文件。一种用于抽象,一种用于实现。如果I在您的环境中正常使用,请使用:(但我不喜欢它:))
奥林斯2012年

在P.SE的情况下,在接口前面加上I可以使发帖人在谈论接口,而不是从其他类继承而变得更加明显。

0

接口的编码可能很糟糕,实际上在某些情况下被视为反模式,而不是法律。通常,当您的接口在应用程序中只有一个实现时,一个很好的例子就是对接口反模式进行编码。另一个可能是,如果接口文件变得如此麻烦,以至于您必须在实现的类的文件中隐藏其声明。


接口的应用程序中只有一种实现不一定是一件坏事;一种可能是保护应用程序不受技术变化的影响。例如,用于数据持久性技术的接口。当前的数据持久性技术只有一个实现,而如果要更改的话,则可以保护应用程序。因此,那时只有一个小鬼。
TSmith

0

将类和接口放入同一文件对如何使用它们都没有限制。接口仍然可以以相同的方式用于模拟等,即使该接口与实现该接口的类在同一文件中也是如此。这个问题纯粹是组织上的便利之一,在这种情况下,我会说做任何使您的生活更轻松的事情!

当然,有人可能会争辩说,将这两个文件放在同一文件中可能会使粗心的人认为它们在编程上比实际更多地耦合在一起,这是一种风险。

就个人而言,如果我遇到了使用一种约定而不是另一种约定的代码库,那么我不会太在意这两种方法。


这不仅是“组织上的便利”,还涉及风险管理,部署管理,代码可追溯性,变更控制,源代码控制噪声,安全管理。这个问题有很多角度。
TSmith
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.