接口的整个观点不是多个类都遵循一组规则和实现吗?
接口的整个观点不是多个类都遵循一组规则和实现吗?
Answers:
严格来说,不,您可以,YAGNI适用。就是说,您花费在创建界面上的时间是最少的,尤其是如果您有一个方便的代码生成工具为您完成大部分工作时。如果您不确定是否需要该接口,我会说最好是支持接口的定义。
此外,即使对于单个类也使用接口,将为您提供单元测试的另一种模拟实现,而不是在生产中。在这一点上,Avner Shahar-Kashtan的答案有所扩展。
我会回答,是否需要一个接口并不取决于实现该接口的类数。接口是用于定义应用程序的多个子系统之间的合同的工具。因此真正重要的是如何将应用程序划分为子系统。无论有多少类实现它们,都应该有接口作为封装子系统的前端。
这是一个非常有用的经验法则:
Foo
直接指向班级BarImpl
,则Foo
每次更改时都坚决要求更改BarImpl
。您基本上将它们视为分成两个类的一个代码单元。Foo
引用界面 Bar
,则是在承诺自己,以避免更改Foo
时进行更改BarImpl
。如果您在应用程序的关键点上定义了接口,则需要仔细考虑它们应该支持和不应该支持的方法,并清楚地注释接口,以描述实现的行为方式(以及如何实现),你的应用将是一个更容易理解,因为这些评论接口将提供某种规范的它是如何在应用程序的描述的意图行事。这使阅读代码变得更加容易(而不是询问“此代码应该做什么”,您可以问“该代码如何完成应做的事情”)。
除了所有这些(或者实际上是因为它)之外,接口还促进了单独的编译。由于接口要比其实现更容易编译并且具有更少的依赖关系,这意味着如果编写类Foo
以使用接口Bar
,则通常可以重新编译BarImpl
而无需重新编译Foo
。在大型应用程序中,这可以节省大量时间。
Foo
取决于接口,Bar
则BarImpl
无需重新编译就可以对其进行修改Foo
。4.比公共/私有产品提供更细粒度的访问控制(将相同的类通过不同的接口提供给两个客户端)。
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 的场景。对于多类方案,我了解依赖倒置原理及其接口的帮助。
指定接口以定义行为,即一组功能/方法的原型。实现接口的类型将实现该行为,因此当您处理此类类型时,您会(部分地)知道其具有的行为。
如果您知道接口定义的行为将仅使用一次,则无需定义接口。吻(保持简单,愚蠢)
从理论上讲,您不应该仅仅为了拥有一个接口而拥有一个接口,Yannis Rizos的答案暗示了进一步的复杂性:
当您编写单元测试并使用Moq或FakeItEasy之类的模拟框架(以命名我最近使用的两个框架)时,您隐式地创建了另一个实现该接口的类。搜索代码或进行静态分析可能会声称只有一个实现,但实际上存在内部模拟实现。每当您开始编写模拟时,您都会发现提取接口是有意义的。
但是等等,还有更多。在更多情况下,存在隐式接口实现。例如,使用.NET的WCF通信堆栈,可以生成远程服务的代理,该代理又可以实现该接口。
在干净的代码环境中,我同意此处的其余答案。但是,请注意可能使用接口的任何框架,模式或依赖项。
不,您不需要它们,我认为为每个类引用自动创建接口是一种反模式。
为所有内容制作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也是如此。
也可以看看:
new Mockup<YourClass>() {}
并且整个类(包括其构造函数)都将被模拟,无论是接口,抽象类还是具体类。如果愿意,还可以“重写”构造函数的行为。我想在Mockito或Powermock中有等效的方法。
看起来栅栏两侧的答案都可以总结为:
设计良好,并将接口放在需要接口的地方。
正如我在对Yanni的回答中所指出的那样,我认为您不可能对接口有一个严格的规定。根据定义,该规则必须灵活。我对接口的规则是,在创建API的任何地方都应使用接口。而且,您应该在跨界从一个责任域到另一个责任域的任何地方创建一个API。
对于(一个非常糟糕的)示例,假设您正在构建一个Car
类。在您的课程中,您肯定需要一个UI层。在这个特定的例子中,它需要的形式IginitionSwitch
,SteeringWheel
,GearShift
,GasPedal
,和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
。在肉类车中,点火开关负责:
这些属性构成了域的IgnitionSwitch
,或者换句话说,它知道并负责。
该IgnitionSwitch
是不负责的GasPedal
。点火开关在任何方面都完全不了解油门踏板。它们都彼此完全独立地运行(尽管没有它们,一辆汽车将毫无价值!)。
如我最初所说,这取决于您的设计。您可以设计一个IgnitionSwitch
具有两个值的:开(True
)和关(False
)。或者,您可以设计它以验证为其提供的密钥以及许多其他操作。这是成为开发人员要决定在哪里划清界限的难点-坦白说,大多数情况下,这是完全相对的。但是,这些内容很重要-那是您的API所在的地方,因此也应该是您界面的所在。
自从您问了这个问题以来,我想您已经看到了隐藏多个实现的接口的好处。这可以通过依赖倒置原理来证明。
但是,是否需要一个接口并不取决于其实现的数量。接口的真正作用是定义一个合同,该合同说明应提供什么服务而不是应如何实现。
合同确定后,两个或多个团队可以独立工作。假设您正在处理模块A,并且它依赖于模块B,那么在B上创建接口的事实使您可以继续工作而不必担心B的实现,因为所有细节都被接口隐藏了。因此,分布式编程成为可能。
即使模块B仅具有其接口的一种实现,该接口仍然是必需的。
总之,界面向用户隐藏了实现细节。对接口进行编程有助于编写更多文档,因为必须定义合同,编写更多模块化软件,促进单元测试并加快开发速度。
这里的所有答案都很好。确实,大多数时候您不需要实现其他接口。但是在某些情况下,您可能仍要这样做。在某些情况下,我会这样做:
该类实现了另一个我不希望通过
桥接第三方代码的适配器类经常发生的接口。
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;
}
因此,最后我使用接口的原因与使用私有字段的原因相同:其他对象不应访问他们不应访问的内容。如果遇到这种情况,即使只有一个类实现该接口,我也会引入一个接口。
接口确实很重要,但是请尝试控制所拥有的接口数量。
在为几乎所有内容创建接口的过程中,最终很容易获得“切碎的意大利面条”代码。我顺应Ayende Rahien的更大智慧,他在这个问题上发表了一些非常明智的话:
http://ayende.com/blog/153889/limit-your-abstractions-analyzing-a-ddd-application
这是他整个系列的第一篇文章,所以请继续阅读!
在这种情况下,您仍可能要引入接口的一个原因是要遵循“ 依赖倒置原则”。也就是说,使用该类的模块将取决于它的抽象(即接口),而不取决于具体的实现。它将高级组件与低级组件分离。