我怎么知道何时创建界面?

时间:2009-01-14 19:03:24

标签: design-patterns oop interface class-design

我正处于开发学习的某个阶段,我觉得我必须更多地了解接口。

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

我已经阅读了一些例子:动物基类,IAnimal界面,如'Walk','Run','GetLegs'等等 - 但我从来没有做过某些事情,感觉就像“嘿我应该在这里使用界面!“

我错过了什么?为什么我要掌握这么难的概念!我只是因为我可能没有意识到对一个人的具体需求而感到恐惧 - 主要是因为他们理解它们时缺少一些方面!这让我觉得自己在成为开发者方面缺少一些东西!如果有人有过这样的经历并取得了突破,我会很感激如何理解这个概念。谢谢。

24 个答案:

答案 0 :(得分:144)

它解决了这个具体问题:

你有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等)。

答案 1 :(得分:129)

我非常喜欢吉米的答案,但我觉得我需要添加一些东西。整个过程的关键是IProcess中的“能力”能力。它表示实现接口的对象的功能(或属性,但意味着“内在质量”,而不是C#属性意义上)。 IAnimal可能不是一个很好的接口示例,但IWalkable可能是一个很好的接口,如果你的系统有许多可以走的东西。您可能拥有源自动物的类,如狗,牛,鱼,蛇。前两个可能会实现IWalkable,后两个不会走路,所以他们不会。现在你问“为什么不只是拥有另一个超类,WalkingAnimal,狗和牛来自哪里?”。答案是当你有一些完全在继承树之外的东西也可以走路时,比如机器人。 Robot会实现IWalkable,但可能不会派生于Animal。如果你想要一个可以走路的东西列表,你可以输入IWalkable,你可以将所有行走的动物和机器人放在列表中。

现在将IWalkable替换为更像IPersistable的软件,这个类比变得更接近你在真实程序中看到的内容。

答案 2 :(得分:69)

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

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

答案 3 :(得分:32)

想想像合约这样的界面。这是一种说法,“这些类应遵循这些规则。”

所以在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就会有效。完全没有关于班级的任何其他知识。“

this related question中也有一个很好的讨论。

答案 4 :(得分:18)

别担心。很多开发人员很少需要编写接口。您经常使用.NET框架中提供的接口,但如果您觉得不需要在短时间内编写接口,那就没什么可错的了。

我总是给某人的例子是你有一个帆船课和V蛇课。他们分别继承了Boat类和Car类。现在说你需要循环遍历所有这些对象并调用它们的Drive()方法。虽然您可以编写如下代码:

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

写起来会简单得多:

((IDrivable)myObject).Drive()

答案 5 :(得分:16)

当你希望能够将单个变量用于多种类型时,Jimmy说得对,但所有这些类型都通过接口声明实现了相同的方法。然后你可以在接口类型变量上调用它们main方法。

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

答案 6 :(得分:15)

我喜欢军队比喻。

警长并不关心您是软件开发人员音乐家还是律师。 您被视为士兵

uml

对于中士而言,更容易不打扰他正在使用的人的具体细节,
把每个人都视为士兵的抽象(......当他们不能像那样行事时惩罚他们)。

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

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

需要抽象细节以实现简单就是回答您的问题。

  

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

     

(..)这使您的代码更容易编写,更容易让其他人理解。它还使您的代码可扩展,因为稍后可以将其他子类添加到类型系列中,并且这些新子类的对象也可以使用现有代码。

答案 7 :(得分:14)

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

我发现还有很多其他原因可以强化重构接口的良好实践,但单元测试/模拟事物提供了实际经验的初步“时刻”。

编辑:为了澄清,通过单元测试和模拟,我总是有两个实现 - 真正的,具体的实现和测试中使用的替代模拟实现。一旦你有两个实现,接口的价值变得明显 - 在接口方面处理它,这样你就可以随时替换实现。在这种情况下,我用模拟界面替换它。我知道如果我的类构造正确,我可以在没有实际接口的情况下执行此操作,但使用实际接口可以强化这一点并使其更清晰(对读者来说更清晰)。如果没有这种推动力,我认为我不会理解接口的价值,因为我的大部分课程都只有一个具体的实现。

答案 8 :(得分:10)

一些非编程示例可能有助于您在编程中看到接口的适当用法。

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

各国都有标准的轨距。这样就可以在铁路工程公司和建造火车的工程公司之间进行分工,并使铁路公司能够更换和升级没有重新架构整个系统的列车。

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

来自大自然的例子:我不太热衷于eats(),makesSound(),move()等示例。他们确实描述了行为,这是正确的,但他们并没有描述互动及其启用方式。在自然界中实现相互作用的界面的明显例子与繁殖有关,例如花为蜜蜂提供某种界面以便可以进行授粉。

答案 9 :(得分:5)

完全有可能以.net开发人员为生,永远不会编写自己的界面。毕竟,我们在没有它们的情况下幸存了好几十年,我们的语言仍然是图灵完成的。

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

  1. 在我们的插件模型中,我们通过接口加载插件,并为插件编写者提供符合的接口。

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

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

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

  5. 我想如果有一条规则,那么当你想在is-a关系中组合几个类时使用接口,但是你不想在基类中提供任何实现。

答案 10 :(得分:5)

一个代码示例(安德鲁与我在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抽象基类)。但是请考虑您需要使用强制您使用基类的第三方记录器(假设它暴露了您需要使用的受保护方法)。由于该语言不支持多重继承,因此无法使用抽象基类方法。

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

答案 11 :(得分:4)

一旦您需要强制为您的班级行为,您就应该定义一个界面。

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

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

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

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

答案 12 :(得分: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中的更改。 (基类也为您带来这种好处。)

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

答案 13 :(得分:3)

假设您想要模拟在您尝试入睡时可能发生的烦恼。

接口之前的模型

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

带接口的模型

为了克服这个问题,我们可以引入一个iterface enter 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();
    }
}

答案 14 :(得分:2)

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

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

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

CLASSIC EXAMPLE:没有实现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时,这一点尤为重要。

答案 15 :(得分:2)

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

http://www.nmock.org/

答案 16 :(得分:2)

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

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

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

如果您稍后决定需要切换到其他提供商,则可以。只需为新提供程序创建另一个具体实现。由于你所依赖的只是你的界面(契约),你可以换掉你的应用程序使用哪一个而不用改变消耗它的代码。

答案 17 :(得分:2)

考虑一下你是第一人称射击游戏。该玩家有多种枪可供选择。

我们可以有一个界面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,并在调用shoot()时调用fire()函数。

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

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

结论

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

例如来自fire()类的Shooter函数需要guns(SniperShotGun)来实现shoot()函数。 所以如果我们换枪并开枪。

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

我们更改了fire()功能的行为。

答案 18 :(得分:1)

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

接口适用于许多方面,其中一个关键因素是可插拔性。例如,声明一个具有List参数的方法将允许传入实现List接口的任何内容,允许开发人员在以后删除并插入不同的列表,而无需重写大量代码。 / p>

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

我建议阅读Coad,Mayfield和Kern在Java Design中的接口章节。他们解释它比平均介绍文本好一点。如果你不使用Java,你可以阅读本章的开头部分,这主要是概念。

答案 19 :(得分:1)

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

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

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

话虽如此,你什么时候需要创建一个界面?

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

它有效:o)

一个例外:当我何时模拟对象时,界面更容易使用。所以我经常为此创建界面。

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

关心,西尔万。

答案 20 :(得分:1)

当您想要定义对象可以展示的行为时,通常会使用接口。

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

(Dispose()方法也由VB.NETC#的使用语言构造调用,该构造仅适用于IDisposable s)

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

答案 21 :(得分:1)

使用界面的目的很多。

  1. 用于多态行为。您希望使用具有对子类的引用的接口调用子类的特定方法的位置。

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

  3. 通过在类中加载特定方法来减少内存使用量。就像你有三个业务对象并且它们是在一个类中实现一样,你可以使用三个接口。

  4. 例如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();
    

答案 22 :(得分:1)

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

Interfaces are a contract一样,没有人提到接口是使代码的某些部分稳定的好方法。当它是团队项目时(或者当您开发其他开发人员使用的代码时),这尤其有用。那么,这是一个具体的场景:

  

当您在团队中开发代码时,其他人可能会使用您编写的代码。当他们为您的(稳定)接口编码时,他们会非常高兴,当您可以自由地更改您的实现(隐藏在界面后面)而不破坏团队的代码时,您会很高兴。它是信息隐藏的变体(接口是公共的,实现对客户端程序员是隐藏的)。详细了解protected variations

另见related question about coding to an Interface

答案 23 :(得分:1)

扩大拉森纳所说的话。接口是所有实现类必须遵循的合同。因此,您可以使用称为编程的技术来签订合同。这使您的软件可以独立于实现。