我怎么知道什么时候创建一个接口?

在我的开发学习中,我觉得我必须学习更多关于接口的知识。

我经常读到它们,但我似乎无法理解它们。

我读过这样的例子:动物基类,IAnimal接口的东西,如“Walk”,“Run”,“GetLegs”等-但我从来没有工作过,觉得“嘿,我应该在这里使用接口!”

我错过了什么?为什么这个概念对我来说这么难理解!我只是害怕这样一个事实,我可能从来没有意识到一个具体的需要-主要是由于一些缺失的理解他们!这让我觉得我作为一名开发人员缺少了一些东西!如果有人有过这样的经历,并取得了突破,我会很感激一些关于如何理解这个概念的建议。谢谢你!

26790 次浏览

把接口想象成一个契约。这是一种说法,“这些类应该遵循这些规则。”

所以在IAnimal的例子中,它是一种说,“我必须能够在实现IAnimal的类上调用Run, Walk等。”

为什么这个有用?您可能希望构建一个函数,该函数依赖于必须能够在对象上调用Run和Walk这一事实。你可以有以下内容:

public void RunThenWalk(Monkey m) {
m.Run();
m.Walk();
}


public void RunThenWalk(Dog d) {
d.Run();
d.Walk();
}

... 对所有你知道能跑能走的物体重复这一步骤。然而,在你的IAnimal接口中,你可以像下面这样定义函数:

public void RunThenWalk(IAnimal a) {
a.Run();
a.Walk();
}

通过根据接口编程,您实际上是信任类来实现接口的目的。所以在我们的例子中,想法是“我不在乎如何他们跑和走,只要他们跑和走。只要他们履行协议,我的RunThenWalk就有效。它在不了解任何其他课程的情况下运行得很好。”

这个相关的问题中也有一个很好的讨论。

它解决了一个具体的问题:

你有a b c d四种不同类型。在你的代码中,你可以这样写:

a.Process();
b.Process();
c.Process();
d.Process();

为什么不让他们实现IProcessable呢

List<IProcessable> list;


foreach(IProcessable p in list)
p.Process();

当你添加50种类型的类,它们都做同样的事情时,这种伸缩性会更好。


另一个具体问题是:

你有没有看过System.Linq.Enumerable?它定义了大量的扩展方法,可以对实现IEnumerable的任何类型进行操作。因为任何实现IEnumerable的东西基本上都在说“我支持无序foreach类型模式中的迭代”,所以你可以为任何可枚举类型定义复杂的行为(Count、Max、Where、Select等)。

扩展一下Larsenal所说的。接口是所有实现类都必须遵循的契约。因此,您可以使用一种称为契约编程的技术。这允许您的软件变得独立于实现。

根据我的经验,直到我开始使用模拟框架进行单元测试,才有了创建接口的动力。很明显,使用接口将使模拟变得更容易(因为框架依赖于方法是虚拟的)。一旦开始,我就看到了从实现中抽象出类的接口的价值。即使我没有创建一个实际的接口,我现在也尝试使我的方法成为虚拟的(提供一个可以覆盖的隐式接口)。

我发现还有许多其他原因可以加强重构到接口的良好实践,但是单元测试/模拟的事情提供了最初的“顿悟时刻”的实践经验。

编辑:澄清一下,在单元测试和模拟中,我总是有两个实现——真实的,具体的实现和测试中使用的替代模拟实现。一旦您有了两个实现,接口的价值就变得显而易见了——从接口的角度处理它,这样您就可以随时替换实现。在本例中,我将它替换为模拟接口。我知道,如果我的类构造正确,我可以在没有实际接口的情况下做到这一点,但使用实际接口可以加强这一点,并使它更清晰(对读者更清晰)。如果没有这种动力,我想我不会欣赏接口的价值,因为我的大多数类都只有一个具体的实现。

当相同功能的实现不同时使用接口。

当你需要共享一个公共的具体实现时,使用一个抽象/基类。

当您希望能够对多个类型使用单个变量,但所有这些类型通过接口声明实现相同的方法时,Jimmy是正确的。然后你可以在接口类型变量上调用它们的main方法。

然而,使用接口还有第二个原因。当项目架构师和实现编码员是不同的人时,或者有几个实现编码员和一个项目经理。负责人可以编写一大堆接口,并查看系统的互操作,然后让开发人员用实现类填充接口。这是确保多人编写兼容类的最佳方法,而且他们可以并行地完成。

别太担心。很多开发人员很少需要编写接口。你会经常使用net框架中可用的接口,但如果你觉得没有必要在短时间内编写一个接口,那就没什么好惊讶的了。

我经常给别人的例子是,如果你有一个帆船职业和一个毒蛇职业。它们分别继承了Boat类和Car类。现在假设你需要遍历所有这些对象并调用它们的Drive()方法。你也可以编写如下代码:

if(myObject is Boat)
((Boat)myObject).Drive()
else
if (myObject is Car)
((Car)myObject).Drive()

这样写会简单得多:

((IDrivable)myObject).Drive()

作为一个。net开发人员,你完全有可能一辈子都不编写自己的接口。毕竟,没有它们,我们也活了几十年,我们的语言仍然是图灵完备的。

我不能告诉你为什么你需要接口,但我可以给你一个我们在当前项目中使用它们的列表:

  1. 在我们的插件模型中,我们通过接口加载插件,并将该接口提供给插件编写者以使其遵循。

  2. 在我们的机间消息传递系统中,消息类都实现了一个特定的接口,并使用该接口“解包装”。

  3. 我们的配置管理系统定义了一个用于设置和检索配置设置的接口。

  4. 我们使用一个接口来避免讨厌的循环引用问题。(如果没有必要,就不要这样做。)

我想如果有一个规则,那就是当你想在一个is-a关系中对几个类进行分组,但你不想在基类中提供任何实现时使用接口。

一旦你需要为你的类强制一个行为,你应该定义一个接口。

动物的行为可能包括走、吃、跑等。因此,您将它们定义为接口。

另一个实际的例子是ActionListener(或Runnable)接口。您可以在需要跟踪特定事件时实现它们。因此,你需要在你的类(或子类)中提供actionPerformed(Event e)方法的实现。类似地,对于Runnable接口,提供public void run()方法的实现。

此外,您可以让任意数量的类实现这些接口。

使用接口(在Java中)的另一个实例是实现c++中提供的多重继承。

最简单的例子就是支付处理器。(Paypal, PDS等)。

假设您创建了一个具有ProcessACH和ProcessCreditCard方法的接口IPaymentProcessor。

现在可以实现一个具体的Paypal实现。让这些方法调用PayPal特定的函数。

如果你决定以后需要换到另一个提供商,你可以这样做。只需为新提供程序创建另一个具体实现。由于您所绑定的只是您的接口(契约),因此您可以在不更改使用它的代码的情况下切换应用程序使用的接口。

我很喜欢吉米的回答,但我觉得我需要补充一些东西。整个事情的关键是IProcess能力中的“able”。它表示实现接口的对象的一种能力(或属性,但指的是“内在质量”,而不是c#属性的意义)。IAnimal可能不是一个很好的接口例子,但是IWalkable可能是一个很好的接口,如果你的系统有很多可以行走的东西。你可能有类派生自动物,如狗,牛,鱼,蛇。前两个可能会实现IWalkable,后两个不会行走,所以它们不会。现在你可能会问“为什么不创建另一个超类WalkingAnimal,就像狗和牛一样?”答案是当你有一些完全在继承树之外也能行走的东西时,比如机器人。Robot将实现IWalkable,但可能不会派生自Animal。如果你想要一个可以走路的东西的列表,你可以输入IWalkable,你可以把所有会走路的动物和机器人都放在列表中。

现在,将IWalkable替换为IPersistable这样更像软件的东西,这样的类比就更接近你在实际程序中看到的情况了。

我偶尔也会使用接口,下面是我最新的用法(名称已经概括了):

我在WinForm上有一堆需要将数据保存到业务对象的自定义控件。一种方法是分别调用每个控件:

myBusinessObject.Save(controlA.Data);
myBusinessObject.Save(controlB.Data);
myBusinessObject.Save(controlC.Data);

这个实现的问题是,每当我添加一个控件,我必须进入我的“保存数据”方法,并添加新的控件。

我改变了我的控件来实现一个ISaveable接口,它有一个方法SaveToBusinessObject(…),所以现在我的“保存数据”方法只是通过控件迭代,如果它发现一个是ISaveable,它调用SaveToBusinessObject。所以现在当需要一个新的控件时,所有人要做的就是在该对象中实现ISaveable(并且永远不要触及其他类)。

foreach(Control c in Controls)
{
ISaveable s = c as ISaveable;


if( s != null )
s.SaveToBusinessObject(myBusinessObject);
}

接口通常未被意识到的好处是本地化修改。定义之后,您很少会更改应用程序的整体流程,但通常会在细节级别上进行更改。当您将细节保存在特定对象中时,ProcessA中的更改将不会影响ProcessB中的更改。(基类也有这个好处。)

编辑:另一个好处是行动的专一性。就像在我的例子中,我所要做的就是保存数据;我不关心它是什么类型的控件,或者它是否可以做任何其他事情——我只想知道我是否可以保存控件中的数据。它使我的保存代码非常清晰——没有检查它是否为文本、数字、布尔值或任何东西,因为自定义控件处理所有这些。

当您希望定义对象可以显示的行为时,通常会使用接口。

在. net世界中一个很好的例子是IDisposable接口,它用于任何使用必须手动释放的系统资源的Microsoft类。它要求实现它的类具有Dispose()方法。

(Dispose()方法也由VB。网c#的using语言结构调用,它只适用于__abc0)

请记住,你可以通过使用诸如TypeOf ... Is (VB.NET)、is (c#)、instanceof (Java)等构造来检查对象是否实现了特定的接口……

可能有些人已经回答过了,接口可以用于在类之间强制执行某些行为,而这些行为不会以相同的方式实现。因此,通过实现一个接口,你的类具有接口的行为。IAnimal接口不是一个典型的接口,因为Dog、Cat、Bird等类都是动物类型,应该扩展它,这是一个继承的例子。相反,在这种情况下,界面应该更像动物行为,如IRunnable, IFlyable, ITrainable等。

接口有很多好处,其中一个关键就是可插拔性。例如,声明一个具有List参数的方法将允许传入实现List接口的任何东西,允许开发人员在以后删除和插入不同的列表,而不必重写大量代码。

您可能永远不会使用接口,但如果您正在从头开始设计一个项目,特别是某种类型的框架,您可能会想要熟悉它们。

我建议阅读Coad, Mayfield和Kern所写的关于Java设计中的接口的章节。它们比一般的介绍性文本解释得好一点。如果你不使用Java,你可以只阅读本章的开头,主要是一些概念。

一些非编程示例可能帮助您了解接口在编程中的适当使用。

电气设备和电网之间有一个接口——它是关于插头和插座的形状以及它们之间的电压/电流的一系列约定。如果您想实现一个新的电气设备,只要您的插头遵循规则,它将能够从网络获得服务。这使得可扩展性非常简单,并删除或降低协调成本:你不必通知电力供应商关于你的新设备如何工作,并就如何将新设备插入网络达成单独的协议。

各国都有标准的铁路轨距。这使得在铺设铁轨的工程公司和建造在这些铁轨上运行的列车的工程公司之间可以实现劳动分工,并且这使得铁路公司可以在不重新构建整个系统的情况下更换和升级列车。

企业向客户提供的服务可以描述为一个接口:一个定义良好的接口强调服务,隐藏手段。当你把一封信放入邮箱时,你期望邮政系统在给定的时间内投递信件,但你对信件如何投递没有期望:你不需要知道,邮政服务有最符合要求和当前情况的投递灵活选择手段。一个例外是客户选择航空邮件的能力——这不是现代计算机程序员会设计的那种界面,因为它暴露了太多的实现。

来自自然的例子:我不太喜欢eats(), makesSound(), moves()等例子。它们确实描述了行为,这是正确的,但它们没有描述交互以及它们是如何被启用的。在自然界中,使相互作用成为可能的界面的一个明显的例子是与繁殖有关的,例如一朵花为蜜蜂提供了一个特定的界面,以便授粉能够发生。

与任何为系统增加灵活性的编程技术一样,接口也增加了一定程度的复杂性。它们通常都很好,而且你可以在任何地方使用它(你可以为你的所有类创建一个接口)——但是这样做,你会创建一个更复杂的系统,更难维护。

像往常一样,这里有一个权衡:灵活性胜过可维护性。哪个更重要?没有答案——这取决于项目本身。但是请记住,每个软件都需要维护……

所以我的建议是:在真正需要接口之前不要使用它们。(使用Visual Studio,你可以在2秒内从现有的类中提取一个接口——所以不要着急。)

话虽如此,你什么时候需要创建一个接口呢?

我在重构突然需要处理两个或多个类似类的方法时这样做。然后创建一个接口,将该接口分配给两个(或多个)类似的类,并更改方法参数类型(将类类型替换为接口类型)。

它是有效的:o)

一个例外:当我模拟对象时,接口更容易使用。我经常为此创建接口。

PS:当我写“接口”时,我的意思是:“任何基类的接口”,包括纯接口类。请注意,抽象类通常比纯接口更好,因为您可以向它们添加逻辑。

问候,迪斯丁。

如果浏览. net Framework程序集并深入到任何标准对象的基类中,您将注意到许多接口(名为ISomeName的成员)。

接口基本上是用于实现框架的,无论框架大小。在我想编写自己的框架之前,我对接口也有同样的感觉。我还发现,理解接口可以帮助我更快地学习框架。当您想要为任何事情编写更优雅的解决方案时,您会发现接口非常有意义。这就像让一个班级穿上适合工作的衣服一样。更重要的是,接口允许系统变得更加自文档化,因为当类实现接口时,复杂的对象变得不那么复杂,这有助于对其功能进行分类。

类在希望能够显式或隐式地参与框架时实现接口。例如,IDisposable是一个公共接口,它为流行且有用的Dispose()方法提供方法签名。在框架中,您或其他开发人员需要知道的关于类的所有信息是,如果它实现了IDisposable,那么您就知道((IDisposable)myObject). dispose()可用于清理目的。

经典示例:在没有实现IDisposable接口的情况下,你不能在c#中使用"using()"关键字构造,因为它要求任何指定为参数的对象都可以隐式转换为IDisposable。

< p >复杂的例子: 一个更复杂的例子是System.ComponentModel.Component类。这个类同时实现了IDisposable和IComponent。大多数(如果不是全部的话). net对象都有一个与之关联的可视化设计器来实现IComponent,这样IDE就能够与组件交互 < p >结论: 随着你越来越熟悉。net框架,当你在对象浏览器或。net Reflector(免费)工具(http://www.red-gate.com/products/reflector/)中遇到一个新类时,你要做的第一件事是检查它从哪个类继承,以及它实现的接口。. net Reflector甚至比对象浏览器更好,因为它也让你看到派生类。这允许您了解从特定类派生的所有对象,从而可能了解您不知道存在的框架功能。当更新或新的名称空间被添加到. net Framework时,这一点尤其重要

它还允许您执行模拟单元测试(. net)。如果您的类使用接口,您可以在单元测试中模拟对象,并轻松地测试逻辑(无需实际触及数据库或web服务等)。

http://www.nmock.org/

一个代码示例(Andrew的代码和我的what-is-the-purpose-of-interfaces代码的组合),也说明了为什么在不支持多重继承的语言(c#和java)上接口而不是抽象类:

interface ILogger
{
void Log();
}
class FileLogger : ILogger
{
public void Log() { }
}
class DataBaseLogger : ILogger
{
public void Log() { }
}
public class MySpecialLogger : SpecialLoggerBase, ILogger
{
public void Log() { }
}

注意,FileLogger和DataBaseLogger不需要接口(可以是Logger的抽象基类)。但是考虑到您需要使用第三方记录器,它迫使您使用基类(假设它公开了您需要使用的受保护的方法)。由于语言不支持多重继承,您将无法使用抽象基类方法。

底线是:尽可能使用接口来获得代码的额外灵活性。您的实现不那么受约束,因此它能更好地适应变化。

使用接口有很多目的。

  1. 用于多态行为。在这里,您希望调用具有对子类引用的接口的子类的特定方法。

  2. 与类有一个契约,在必要的地方实现所有的方法,就像最常见的使用是COM对象,其中包装器类在继承接口的DLL上生成;这些方法是在幕后调用的,你只需要实现它们,但是使用与COM DLL中定义的相同的结构,你只能通过它们公开的接口来知道。

  3. 通过加载类中的特定方法来减少内存使用。例如,如果您有三个业务对象,并且它们在单个类中实现,则可以使用三个接口。

例如IUser, IOrder, IOrderItem

public interface IUser()
{


void AddUser(string name ,string fname);


}


// Same for IOrder and IOrderItem
//




public class  BusinessLayer: IUser, IOrder, IOrderItem


{
public void AddUser(string name ,string fname)
{
// Do stuffs here.
}


// All methods from all interfaces must be implemented.


}

如果你只想添加一个用户,可以这样做:

IUser user = new (IUser)BusinessLayer();


// It will load  all methods into memory which are declared in the IUser interface.


user.AddUser();

我喜欢军队的比喻。

中士不在乎你是软件开发人员音乐家还是律师
你将被视为士兵.

.

uml

它是更容易,中士不必费心与他一起工作的人的具体细节,
把每个人都当作抽象的士兵(…当他们不能像孩子一样行事时,就惩罚他们)

人们像士兵一样行动的能力被称为多态性。

接口是帮助实现多态的软件结构。

需要抽象细节,以实现简单是对你的问题的回答。

多态性,在词源上是“多种形式”的意思;是将基类的任何子类的对象视为基类的对象的能力。因此,基类有多种形式:基类本身和它的任何子类。

(. .)这使你的代码更容易编写,也更容易被别人理解。它还使您的代码具有可扩展性,因为稍后可以将其他子类添加到类型家族中,并且这些新子类的对象也可以与现有代码一起工作。

当你成为库开发人员(为其他编码员编码的人)时,接口将变得明显。我们大多数人都从应用程序开发人员开始,在那里我们使用现有的api和编程库。

接口是一个契约相同,还没有人提到接口是使代码的某些部分稳定的的好方法。当它是团队项目时(或者当你正在开发其他开发人员使用的代码时),这尤其有用。所以,这里有一个具体的场景:

当你在团队中开发代码时,其他人可能会使用你写的代码。当他们为你的(稳定的)接口编写代码时,他们会非常高兴,而当你可以自由地改变你的实现(隐藏在接口后面)而不破坏你团队的代码时,你也会非常高兴。它是信息隐藏的变体(接口是公共的,实现对客户端程序员隐藏)。阅读更多关于受保护的变化

也可以看到这个关于接口编码的相关问题

假设你正在制作一款第一人称射击游戏。玩家有多种枪可供选择。

我们可以有一个接口Gun,它定义了一个函数shoot()

我们需要Gun类的不同子类,即ShotGun Sniper等等。

ShotGun implements Gun{
public void shoot(){
\\shotgun implementation of shoot.
}
}


Sniper implements Gun{
public void shoot(){
\\sniper implementation of shoot.
}
}

射击类

射手把所有的枪都装在他的盔甲里。让我们创建一个List来表示它。

List<Gun> listOfGuns = new ArrayList<Gun>();

射手在需要时使用switchGun()函数循环使用他的枪

public void switchGun(){
//code to cycle through the guns from the list of guns.
currentGun = //the next gun in the list.
}

我们可以使用上面的函数设置当前Gun,并在调用fire()时简单地调用shoot()函数。

public void fire(){
currentGun.shoot();
}

shoot函数的行为将根据Gun接口的不同实现而有所不同。

结论

当类函数依赖于来自另一个类的函数时,根据实现的类的实例(对象)创建接口,而来自另一个类的函数会改变其行为。

,例如Shooter类的fire()函数期望枪械(SniperShotGun)实现shoot()函数。 如果我们换枪开火。

shooter.switchGun();
shooter.fire();

我们改变了fire()函数的行为。

假设你想要模拟当你试图睡觉时可能发生的烦恼。

接口前的模型

enter image description here

class Mosquito {
void flyAroundYourHead(){}
}


class Neighbour{
void startScreaming(){}
}


class LampJustOutsideYourWindow(){
void shineJustThroughYourWindow() {}
}

正如你清楚地看到的,当你试图睡觉时,许多“事情”都可能令人讨厌。

使用没有接口的类

但是在使用这些类时,我们遇到了一个问题。他们毫无共同之处。您必须分别调用每个方法。

class TestAnnoyingThings{
void testAnnoyingThinks(Mosquito mosquito, Neighbour neighbour, LampJustOutsideYourWindow lamp){
if(mosquito != null){
mosquito.flyAroundYourHead();
}
if(neighbour!= null){
neighbour.startScreaming();
}
if(lamp!= null){
lamp.shineJustThroughYourWindow();
}
}
}

带有接口的模型

为了克服这个问题,我们可以引入iterfaceenter image description here

interface Annoying{
public void annoy();


}

并在类中实现它

class Mosquito implements Annoying {
void flyAroundYourHead(){}


void annoy(){
flyAroundYourHead();
}
}


class Neighbour implements Annoying{
void startScreaming(){}


void annoy(){
startScreaming();
}
}


class LampJustOutsideYourWindow implements Annoying{
void shineJustThroughYourWindow() {}


void annoy(){
shineJustThroughYourWindow();
}
}

接口使用

这将使这些类的使用更容易

class TestAnnoyingThings{
void testAnnoyingThinks(Annoying annoying){
annoying.annoy();
}
}