我如何知道何时创建界面?


196

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

我经常阅读有关它们的信息,但似乎我无法掌握它们。

我已经阅读了以下示例:Animal基类,带有IAnimal接口,用于处理“ Walk”,“ Run”,“ GetLegs”等内容-但我从未从事过某些工作,并且感觉像“嘿,我应该使用接口这里!”

我想念什么?为什么我很难理解这个概念!我可能从未意识到具体的需求,这一事实使我感到震惊-主要是由于缺乏理解它们的方面!这让我觉得自己在成为一名开发人员方面缺少了什么!如果有人有这样的经历并取得了突破,我将不胜感激如何理解这一概念的一些技巧。谢谢。


Answers:


151

它解决了这个具体问题:

您有4种不同类型的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等)。


2
确实有帮助。有一个接口而不是让所有类型都具有Process()方法的实现的优点是什么?
user53885

除非它们都是相同基类型的所有子类,或者它们实现了接口,否则您将无法使用相同的变量p。
卡尔

相对于使用Process方法的50个不同的类,您不必强制转换。C#不使用“鸭子类型”,所以仅仅因为A具有Process()而B具有Process()并不意味着有任何通用的方法来调用。您需要一个接口。
user7116

对。我只是将“ var”更改为“ IProcessable”以使该示例更有意义。
吉米,

2
@Rogerio:我试图变得通用。关键不是“当您拥有带有Process()函数的东西时”,而是“当您拥有共享一组共同方法的东西时”。该示例可以轻松更改为foreach(IMyCompanyWidgetFrobber a in list) a.Frob(widget, context);
Jimmy 2010年

133

我非常喜欢吉米的答案,但是我觉得需要添加一些内容。整个事情的关键是对的iProcess“能够” 。它指示实现接口的对象的功能(或属性,但含义是“固有质量”,而不是C#属性)。IAnimal可能不是一个接口的好例子,但是如果您的系统有很多东西可以走,则IWalkable可能是一个好的接口。您可能有从动物派生的类,例如狗,牛,鱼,蛇。前两个可能会实现IWalkable,后两个不会走路,因此不会。现在,您问“为什么不只拥有另一个超类,WalkingAnimal,这就是“狗和牛”的派生?”。答案是当您在继承树之外完全可以行走的东西(例如机器人)时。机器人将实现IWalkable,但可能不会源自Animal。如果您想要列出可以行走的事物,

现在,用诸如IPersistable之类的更多软件来代替IWalkable,这样的类比就变得与您在真实程序中看到的更加接近。


9
我喜欢您的想法-“这表示能力”。当您必须定义功能时,确实需要接口,因为基础必须坚持其“ Base”类。
拉米兹·乌丁

71

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

需要共享通用的具体实现时,请使用抽象/基类。


8
第一个称为多态性。第二个是蛇油-除非sublcass 基类(不违反Liskov替换原理),否则您应该更喜欢组合而不是继承。
2011年

@ArnisLapsa我不太明白“除非sublcass是基类”的意思。什么时候子类不能“成为”基类?(如is关键字中所示)
Marc.2377'2

32

可以将接口视为合同。可以这样说:“这些类应遵循这些规则集。”

因此,在IAnimal示例中,它是一种表达方式:“我必须能够在实现IAnimal的类上调用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才有效。它运行得很好,无需了解其他任何信息班上。”

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


1
不要忘记实现独立性。接口允许人们不在乎底层函数是如何实现的,只是它照界面所说的去做即可。
马修·布鲁贝克

18

不用担心 很多开发人员几乎不需要编写接口。您将经常使用.NET框架中提供的接口,但是如果您觉得自己不需要很快写任何东西,这也就不足为奇了。

我总是给某人的例子是,如果您有Sailboat类和Viper类。他们分别继承了Boat类和Car类。现在说您需要遍历所有这些对象并调用它们的Drive()方法。虽然可以编写如下代码:

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

编写起来会简单得多:

((IDrivable)myObject).Drive()

16

如果您希望能够对多个类型使用单个变量,而所有这些类型都通过接口声明实现相同的方法,那么Jimmy就是对的。然后,您可以在接口类型变量上将它们称为main方法。

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


15

我喜欢军队的比喻。

中士不在乎您是软件开发者音乐家还是律师
你被当兵了

嗯

这是比较容易的中士不要与他正在与,人的具体细节费心
地对待每一个人作为军人的抽象(...和惩罚他们时,他们无法像那些)。

人们扮演士兵的能力被称为多态。

接口是有助于实现多态性的软件结构。

为了实现简单性,需要抽象细节是您的问题的答案。

多态性在词源上意为“许多形式”,是一种将基类的任何子类的对象视为基类的对象的能力。因此,基类具有多种形式:基类本身及其任何子类。

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


14

以我的经验,直到我开始使用模拟框架进行单元测试时,才开始创建接口。非常清楚的是,使用接口将使模拟变得更加容易(因为框架依赖于虚拟方法)。一旦开始,我就看到了从实现中抽象出与类的接口的价值。即使我没有创建实际的接口,也要尝试使我的方法虚拟化(提供可被覆盖的隐式接口)。

我发现还有很多其他原因可以加强对接口进行重构的良好实践,但是单元测试/模拟是提供实践经验的初始“啊哈时刻”的原因。

编辑:要澄清一下,对于单元测试和模拟,我总是有两个实现-实际的,具体的实现和测试中使用的替代模拟实现。一旦有了两个实现,该接口的价值就变得显而易见-就接口而言对其进行处理,以便您可以随时替换该实现。在这种情况下,我将其替换为模拟接口。我知道,如果我的班级结构正确,我可以在没有实际接口的情况下执行此操作,但是使用实际接口可以加强此功能并使它更干净(对读者来说更清晰)。没有这种动力,我认为我不会欣赏接口的价值,因为只有我的大多数类都曾经有一个具体的实现。


出于错误原因而做对的事情。以您的情况为准-您最终得到了所谓的“头接口”,并且增加了复杂性,而权重实现了简单性。
阿尔尼斯Lapsa

@Arnis-单元测试是“错误的原因”,轻松模拟类以删除测试中的依赖项是“错误的原因”。对不起,但我不同意。
tvanfosson 2011年

1
测试应该通过提供反馈(无论代码是否可测试)间接影响设计。添加可扩展性点以提高可测试性本身就像作弊。我认为Mark Seeman最好地总结一下bit.ly/esi8Wp
Arnis Lapsa 2011年

1
@Arnis-那么您是否在单元测试中使用了模拟?如果没有,您如何消除对依赖性的依赖。您是否正在使用DI?单元测试使我不得不使用模拟和DI。模拟和DI证明了使用接口以一种学术上无法理解的方式定义合同的良好实践的价值。由于我采用了TDD,因此我的代码与以前相比没有那么多耦合。我认为那是一件好事。
tvanfosson 2011年

只是说不是沿着所谓的自然节点进行分解会导致低内聚和不必要的复杂性。
阿尔尼斯Lapsa

10

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

电气设备和电网之间存在一个接口-这是关于插头和插座的形状以及它们之间的电压/电流的一组约定。如果您要实施新的电气设备,只要您的插头遵循规则,它将能够从网络获得服务。这使扩展性非常容易,并且消除或降低了协调成本:您不必将新设备的工作方式通知电力供应商,也不必另行商定如何将新设备插入网络。

各国都有标准的轨距规。这允许在放下铁轨的工程公司与制造在这些铁轨上运行的火车的工程公司之间进行分工,这使铁路公司可以在不重新配置整个系统的情况下更换和升级火车。

企业提供给客户的服务可以描述为界面:定义明确的界面强调服务并隐藏了手段。当您将信件放到邮箱中时,您希望邮政系统在给定的时间内递送信件,但是您对信件的递送方式没有期望:您不需要知道,并且邮政服务可以灵活地选择最能满足要求和当前情况的交付方式。例外是客户选择航空邮件的能力-这不是现代计算机程序员设计的那种界面,因为它揭示了太多的实现。

自然界的例子:我不太喜欢eats(),makeSound(),moves()等例子。它们确实描述了行为,这是正确的,但它们没有描述交互以及如何启用交互。能够在自然界中进行交互的界面的明显示例与繁殖有关,例如,花朵为蜜蜂提供了一定的界面,从而可以进行授粉。


5

完全有可能成为.net开发人员一生,而不用编写自己的界面。毕竟,即使没有它们,我们也能幸存下来数十年,而我们的语言仍然是图灵完备的。

我无法告诉您为什么需要接口,但是可以给您列出我们在当前项目中使用接口的位置:

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

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

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

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

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


5

一个代码示例(Andrew与其他功能在我的接口上的结合),也说明了为什么接口而不是不支持多重继承的语言上的抽象类(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抽象基类)。但是考虑到您需要使用强制您使用基类的第三方记录器(可以说它公开了您需要使用的受保护方法)。由于该语言不支持多重继承,因此您将无法使用抽象基类方法。

最重要的是:在可能的情况下使用接口来获得代码的额外灵活性。您的实现没有那么多束缚,因此可以更好地适应变化。


4

我不时使用接口,这是我的最新用法(名称已通用化):

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

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

此实现的问题在于,每当我添加控件时,都必须进入“保存数据”方法并添加新控件。

我更改了控件以实现具有方法SaveToBusinessObject(...)的ISaveable接口,因此现在我的“保存数据”方法仅遍历控件,并且如果找到一个名为ISaveable的控件,则调用SaveToBusinessObject。因此,现在当需要一个新控件时,某人要做的就是在该对象中实现ISaveable(并且永远不要触摸另一个类)。

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

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

通常,接口无法实现的好处是您可以本地化修改。定义后,您几乎不会更改应用程序的整体流程,但经常会在详细信息级别进行更改。当您将详细信息保留在特定对象中时,ProcessA中的更改不会影响ProcessB中的更改。(基类也为您提供此好处。)

编辑:另一个好处是行动的专一性。就像在我的示例中一样,我要做的就是保存数据。我不在乎它是什么类型的控件,或者它是否还能执行其他操作-我只想知道是否可以将数据保存在控件中。这使我的保存代码非常清晰-不会检查它是文本,数字,布尔值还是其他类型,因为自定义控件会处理所有这些。


4

一旦需要为类强制执行某种行为,就应该定义一个接口。

动物的行为可能涉及步行,进食,跑步等。因此,将它们定义为接口。

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

同样,您可以通过任意数量的类来实现这些接口。

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


3
求上帝使他们停止说关于接口的多重继承之类的事情。您不要继承类中的接口。你IMPLEMENT它。
AndreiRînea,2009年

4

假设您想为烦恼建模,当您尝试入睡时可能会发生。

接口之前建模

在此处输入图片说明

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

带接口的模型

为了克服这个问题,我们可以引入一个斜面在此处输入图片说明

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

好的,但是不是NeighbourLampJustOutsideYourWindow也必须实施Annoying吗?
星尘

是的,谢谢你指出这一点。我对此更改进行了编辑
Marcin Szymczak

2

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

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

您现在可以实现具体的Paypal实现。使这些方法调用PayPal特定功能。

如果您以后决定需要切换到其他提供商,则可以。只需为新提供者创建另一个具体实现。由于您所绑定的只是接口(合同),因此您可以交换应用程序使用的接口,而无需更改使用该接口的代码。


2

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

http://www.nmock.org/


2

如果浏览.NET Framework程序集并向下钻取任何标准对象的基类,则会注意到许多接口(名为ISomeName的成员)。

接口基本上是用于实现框架的(大小)。在我想编写自己的框架之前,我对接口的感觉也一样。我还发现,了解界面可以帮助我更快地学习框架。当您想为几乎所有内容编写一个更优雅的解决方案时,您会发现接口很有意义。这就像让班级穿上适合工作的衣服的方法。更重要的是,接口使系统变得更加自我记录,因为当类实现接口时,复杂的对象变得不那么复杂,这有助于对其功能进行分类。

当类希望能够显式或隐式地参与框架时,它们实现接口。例如,IDisposable是一个通用接口,为流行且有用的Dispose()方法提供方法签名。在框架中,您或其他开发人员需要了解的所有类是,如果它实现IDisposable,则您知道可以使用((IDisposable)myObject).Dispose()进行清理。

典型示例:如果不实现IDisposable接口,则不能在C#中使用“ using()”关键字构造,因为它要求任何指定为参数的对象都可以隐式转换为IDisposable。

复杂示例:一个更复杂的示例是System.ComponentModel.Component类。此类同时实现IDisposable和IComponent。大多数(如果不是全部)具有与之关联的可视设计器的.NET对象都实现了IComponent,以便IDE能够与该组件进行交互。

结论:随着您对.NET Framework的熟悉,在对象浏览器或.NET Reflector(免费)工具(http://www.red-gate.com)中遇到新类时,您将要做的第一件事。 / products / reflector /)将检查其继承自哪个类,以及实现的接口。.NET Reflector甚至比对象浏览器更好,因为它还使您也可以看到Derived类。这样一来,您就可以了解派生自特定类的所有对象,从而有可能了解您不知道存在的框架功能。当将更新的名称空间或新的名称空间添加到.NET Framework时,这尤其重要。


2

考虑您正在制作第一人称射击游戏。玩家有多种枪支可供选择。

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

我们需要类的不同子GunShotGun 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,并shoot()fire()被调用时简单地调用 函数。

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

拍摄功能的行为将根据Gun界面的不同实现而变化。

结论

当一个类函数依赖于另一个类的函数时,创建一个接口,该类将根据实现的类的instance(object)更改其行为。

例如,fire()来自Shooter类的函数期望guns(SniperShotGun)实现该shoot()函数。因此,如果我们开枪开火。

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

我们已经改变了fire()功能的行为。



1

当您要定义对象可以表现的行为时,通常使用接口。

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

(Dispose()方法也由VB.NETC#的使用语言构造调用,仅在IDisposables上有效)

请记住,您可以使用TypeOf ... Is(VB.NET),is(C#),instanceof(Java)等构造来检查对象是否实现了特定接口。


1

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

接口对许多事物都有好处,其中关键之一就是可插拔性。例如,声明一个具有List参数的方法将允许传递实现List接口的所有内容,从而允许开发人员在以后的时间删除并插入其他列表,而无需重写大量代码。

可能永远不会使用接口,但是如果您是从头开始设计项目的,尤其是某种框架,则可能需要熟悉它们。

我建议阅读Coad,Mayfield和Kern撰写的有关Java Design接口的章节。他们的解释比一般介绍性文本要好一些。如果您不使用Java,则可以阅读本章的开头,主要是概念。


1

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

像往常一样,这里需要权衡:灵活性大于可维护性。哪一个更重要?没有答案-这取决于项目。但是请记住,每个软件都必须维护...

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

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

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

它的工作原理:o)

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

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

问候,西尔万。


1

当您成为库开发人员(为其他编码人员编码的人)时,界面将变得显而易见。我们大多数人都是从应用程序开发人员开始的,我们在其中使用现有的API和编程库。

接口是契约一样,没有人提到接口是使代码的某些部分稳定的好方法。当它是团队项目时(或在开发其他开发人员使用的代码时),这尤其有用。因此,这是一个适合您的具体方案:

团队中开发代码时,其他人可能会使用您编写的代码。当他们为您的(稳定)接口编写代码时,他们会感到最高兴;而当您可以自由更改自己的实现(隐藏在接口后面)而又不会破坏团队的代码时,他们将会感到高兴。这是信息隐藏的一种变体(接口是公共的,实现对客户端程序员是隐藏的)。详细了解受保护的版本

另请参阅有关编码到接口的相关问题


1

使用接口有许多目的。

  1. 用于多态行为。您想使用带有对子类的引用的接口来调用子类的特定方法的地方。

  2. 与类达成协议以在必要时实现所有方法,例如最常见的用法是与COM对象一起使用,在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();
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.