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

问题描述 投票:188回答:24

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

我经常阅读它们,但似乎我无法掌握它们。

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

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

design-patterns oop interface class-design
24个回答
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等)。


5
投票

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

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

  1. 在我们的插件模型中,我们通过接口加载插件,并提供插件编写器的接口以符合。
  2. 在我们的intermachine消息系统中,消息类都实现了一个特定的接口,并使用该接口“解包”。
  3. 我们的配置管理系统定义了用于设置和检索配置设置的界面。
  4. 我们有一个接口用于避免令人讨厌的循环引用问题。 (如果你不需要,不要这样做。)

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


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

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


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
投票

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

接口前的模型

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

带接口的模型

为了解决这个问题,我们可以介绍一个iterfaceqazxsw poi

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

2
投票

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

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

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

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


2
投票

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

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


2
投票

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

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

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

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

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

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


2
投票

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

我们可以有一个接口http://www.red-gate.com/products/reflector/,它定义了一个函数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()

我们可以使用上面的函数设置当前的Gun,并在调用public void switchGun(){ //code to cycle through the guns from the list of guns. currentGun = //the next gun in the list. } 时简单地调用shoot()函数。

fire()

拍摄功能的行为将根据public void fire(){ currentGun.shoot(); } 界面的不同实现而变化。

结论

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

例如来自Gun类的fire()函数期望枪支(ShooterSniper)实现ShotGun功能。所以,如果我们切换枪和火。

shoot()

我们改变了shooter.switchGun(); shooter.fire(); 函数的行为。


1
投票

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


129
投票

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

现在用更多软件 - 例如IPersistable替换IWalkable,这个类比变得更接近你在真实程序中看到的东西。


1
投票

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

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

(Dispose()方法也由IDisposableVB.NET的使用语言构造调用,该构造仅适用于C#s)

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


1
投票

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

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

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

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


1
投票

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

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

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

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

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

它有效:o)

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

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

此致,西尔万。


1
投票

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

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

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

另见protected variations


1
投票

使用界面有很多目的。

  1. 用于多态行为。您希望使用具有对子类的引用的接口调用子类的特定方法的位置。
  2. 与类签订合同以实现必要的所有方法,就像最常见的用途是COM对象,其中在继承接口的DLL上生成包装类;这些方法在幕后调用,你只需要实现它们,但结构与COM DLL中定义的结构相同,只能通过它们公开的接口知道。
  3. 通过在类中加载特定方法来减少内存使用量。就像你有三个业务对象并且它们是在一个类中实现一样,你可以使用三个接口。

例如IUser,IOrder,IOrderItem

related question about coding to an Interface

如果您只想添加用户,请执行以下操作:

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.

}

69
投票

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

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


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也有一个很好的讨论。


18
投票

别担心。很多开发人员很少需要编写接口。你会经常使用.NET框架中提供的接口,但是如果你觉得不需要很快就能编写一个接口那么就没什么可惊讶的了。

我总是给某人的例子是你有一个帆船类和一个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
投票

我喜欢军队的比喻。

如果您是软件开发人员,音乐家或律师,警长并不在意。 你被视为士兵。

中士更容易不打扰他正在与之合作的人的具体细节, 把每个人视为士兵的抽象(......当他们不像那些人那样行事时惩罚他们)。

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

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

需要抽象细节才能实现简单才能回答您的问题。

Polymorphism,在词源上意为“多种形式”,是能够处理基类的任何子类的对象,就好像它是基类的对象一样。因此,基类有许多形式:基类本身及其任何子类。

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


14
投票

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

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

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


10
投票

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

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

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

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

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

© www.soinside.com 2019 - 2024. All rights reserved.