【问题标题】:Understanding Interfaces了解接口
【发布时间】:2010-11-05 17:46:29
【问题描述】:

我仍然无法理解接口的用途。我阅读了一些教程,但我仍然不知道它们的真正用途是“它们让你的类信守承诺”和“它们有助于多重继承”。

就是这样。我仍然不知道我什至何时会在实际工作示例中使用接口,甚至何时确定何时使用它。

根据我对接口的有限了解,它们可以提供帮助,因为如果有东西实现它,那么您可以只传递接口以允许传入不同的类,而不必担心它不是正确的参数。

但我永远不知道这样做的真正意义是什么,因为他们通常会在这一点上停止显示代码在通过接口后会做什么,如果他们这样做了,他们似乎没有做任何有用的事情我可以看着然后去“哇,他们会在现实世界的例子中提供帮助”。

所以我想我想说的是我正在尝试找到一个真实世界的示例,我可以在其中看到正在运行的界面。

我也不明白你可以像这样引用对象:

ICalculator myInterface = new JustSomeClass();

所以现在如果我去 myInterface dot 和 intellisense 会拉起来,我只会看到接口方法,而不是 JustSomeClass 中的其他方法。所以我还没有看到这一点。

我还开始在他们似乎喜欢使用接口的地方进行单元测试,但我仍然不明白为什么。

比如这个例子:

public AuthenticationController(IFormsAuthentication formsAuth)
{
    FormsAuth = formsAuth ?? new FormsAuthenticationWrapper();
}

public class FormsAuthenticationWrapper : IFormsAuthentication
{
    public void SetAuthCookie(string userName, bool createPersistentCookie)
    {
        FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);
    }
    public void SignOut()
    {
        FormsAuthentication.SignOut();
    }
}

public IFormsAuthentication FormsAuth
{
    get;
    set;
}

为什么要费心制作这个界面?为什么不直接使用其中的方法制作 FormsAuthenticationWrapper 并收工?为什么先做一个接口,然后让 Wrapper 实现接口,最后再写方法?

那我不明白这句话的真正含义。

就像我现在知道声明是这样说的

FormsAuth = formsAuth ?? new FormsAuthenticationWrapper();

如果 formsAuth 为 null,则创建一个新的 FormsAuthenticationWrapper,然后将其分配给作为接口的属性。

我想这可以追溯到为什么引用事物的全部意义。特别是在这种情况下,因为所有方法都完全相同。 Wrapper 没有接口没有的任何新方法,我不确定,但是当你这样做时,方法被正确填充(即它们有一个主体)它们不会被转换为存根,因为那看起来真的毫无意义对我来说(它将被转换回接口)。

然后在测试文件中他们有:

var formsAuthenticationMock = new Mock<AuthenticationController.IFormsAuthentication>();

所以他们只是传入 FormsAuthentication ,我猜这是所有假存根。我猜包装类是在程序实际运行时使用的,因为它有真正的方法可以做一些事情(比如注销一个人)。

但是查看新的 Mock(来自 moq)它接受一个类或一个接口。为什么不再次让包装类将这些方法放入然后在新的 Mock 调用中呢?

这不只是为您制作存根吗?

【问题讨论】:

标签: c# .net asp.net-mvc interface


【解决方案1】:

好吧,我一开始也很难理解,所以别担心。

想一想,如果你有一个班级,可以说是一个视频游戏角色。

public class Character
{
}

现在说我想让角色拥有武器。我可以使用接口来确定武器所需的方法:

interface IWeapon
{
    public Use();
}

所以让我们给角色一把武器:

public class Character
{
    IWeapon weapon;

    public void GiveWeapon(IWeapon weapon)
    {
        this.weapon = weapon;
    }

    public void UseWeapon()
    {
        weapon.Use();
    }
}

现在我们可以创建使用 IWeapon 接口的武器,我们可以将它们提供给任何角色类,并且该类可以使用该物品。

public class Gun : IWeapon
{
    public void Use()
    {
        Console.Writeline("Weapon Fired");
    }
}

然后你可以把它粘在一起:

Character bob = new character();
Gun pistol = new Gun();
bob.GiveWeapon(pistol);
bob.UseWeapon();

现在这是一个普遍的例子,但它提供了很大的力量。如果您查看策略模式,您可以了解更多信息。

【讨论】:

  • 伟大的例子伴侣。我也在尝试学习使用接口,但到目前为止我很难做到这一点。这个简单但正如你所说的非常强大的例子,只是给了我一些 quidlines。真喜欢它。干杯。
  • +1 也帮助了我!我发现很难理解 foobar 等的通用用法,但这个简单的主题示例至少帮助我理解了更多
  • 在了解接口 6 年后,我现在终于明白了!!!谢谢你的例子!
  • 即使使用抽象类而不是接口,这仍然有效。我自己也很困惑如何确定我们应该使用接口还是抽象类(除了需要多重继承)。
【解决方案2】:

接口定义合约

在您提供的示例中,?? 运算符仅在您将 null 传递给构造函数时提供默认值,并且与接口没有任何关系。

更相关的是,您可以使用实际的 FormsAuthenticationWrapper 对象,但您也可以实现自己的 IFormsAuthentication 类型,它与包装类完全无关。该接口告诉您需要实现哪些方法和属性来履行合同,并允许编译器验证您的对象是否确实遵守该合同(在某种程度上 - 名义上遵守合同很简单,但精神上却没有) ,因此如果您不想使用,则不必使用预构建的 FormsAuthenticationWrapper。您可以构建一个工作方式完全不同但仍遵守所需合同的不同类。

在这方面,接口很像普通的继承,但有一个重要区别。在 C# 中,一个类只能从一种类型继承,但可以实现多个接口。因此,接口允许您在一个类中履行多个合同。一个对象可以一个 IFormsAuthentication 对象,也可以是别的东西,比如 IEnumerable。

当您从另一个方向看时,接口会更加有用:它们允许您将许多不同的类型视为相同。一个很好的例子是各种集合类。拿这个代码示例:

void OutputValues(string[] values)
{
   foreach (string value in values)
   {
       Console.Writeline(value);
   }
}

这接受一个数组并将其输出到控制台。现在应用这个简单的更改来使用接口:

void OutputValues(IEnumerable<string> values)
{
   foreach (string value in values)
   {
       Console.Writeline(value);
   }
}

这段代码仍然接受一个数组并将其输出到控制台。但它也需要List&lt;string&gt; 或您关心的任何其他实现IEnumerable&lt;string&gt; 的东西。因此,我们采用了一个接口并使用它来使一个简单的代码块更加更强大。

另一个很好的例子是 ASP.Net 成员资格提供程序。您告诉 ASP.Net,您通过实现所需的接口来遵守会员合同。现在您可以轻松自定义内置的 ASP.Net 身份验证以使用任何源,这一切都归功于接口。 System.Data 命名空间中的数据提供者以类似的方式工作。

最后一点:当我看到一个带有“默认”包装器实现的接口时,我认为它有点像反模式,或者至少是代码异味。它向我表明,接口可能过于复杂,您要么需要将其拆分,要么考虑使用组合 + 事件 + 委托的组合而不是派生来完成相同的事情。

【讨论】:

    【解决方案3】:

    或许深入了解接口的最佳方法是使用 .NET 框架中的示例。

    考虑以下函数:

    void printValues(IEnumerable sequence)
    {
        foreach (var value in sequence)
            Console.WriteLine(value);
    }
    

    现在我可以编写这个函数来接受List&lt;T&gt;object[] 或任何其他类型的具体序列。但是由于我已经编写了这个函数来接受IEnumerable 类型的参数,这意味着我可以将任何具体类型传递给这个实现IEnumerable 接口的函数。

    这是可取的原因是通过使用接口类型,您的函数比其他方式更灵活。此外,您正在增加此函数的实用性,因为许多不同的调用者将能够使用它而无需修改。

    通过使用接口类型,您可以将参数的类型声明为传入的任何具体类型所需的合同。在我的示例中,我不关心您传递给我的类型,我只关心我可以迭代它。

    【讨论】:

      【解决方案4】:

      这里的所有答案都很有帮助,我怀疑我可以添加任何新的东西,但是在阅读这里的答案时,两个不同答案中提到的两个概念在我的脑海中确实很好地融合了,所以我会撰写我的理解希望对您有所帮助。

      一个类有方法和属性,一个类的每个方法和属性都有一个签名和一个主体

      public int Add(int x, int y)
      {
      return x + y;
      }
      

      Add 方法的签名是第一个花括号字符之前的所有内容

      public int Add(int x, int y)
      

      方法签名的目的是为方法分配一个名称,并描述它的保护级别(公共、受保护、内部、私有和/或虚拟),它定义了可以从代码中访问方法的位置

      签名还定义了方法返回值的类型,上面的 Add 方法返回一个 int,以及方法期望调用者传递给它的参数

      方法通常被认为是一个对象可以做的事情,上面的例子暗示了方法定义的类与数字一起使用

      方法体(在代码中)精确地描述了对象如何执行方法名称所描述的操作。在上面的示例中,add 方法通过将加法运算符应用于其参数并返回结果来工作。

      接口和类在语言语法方面的主要区别之一是接口只能定义方法的签名,而不能定义方法体。

      换句话说,接口可以描述类的动作(方法),但绝不能描述如何执行动作。

      现在您希望对接口是什么有了更好的理解,我们可以继续讨论您问题的第二和第三部分,何时以及为什么我们会在实际程序中使用接口。

      在程序中使用接口的主要时间之一是当人们想要执行某项操作时,不想知道或与如何执行这些操作的具体细节相关联。

      这是一个非常抽象的概念,所以也许举个例子可以帮助你牢牢记住这一点

      想象一下,你是一个非常流行的网络浏览器的作者,每天有数百万人在使用它,你有来自人们的数千个功能请求,有些大,有些小,有些好,有些像“带回 &lt;maquee&gt; 和 @ 987654324@支持”。

      由于您只有相对较少的开发人员,而且一天中的工作时间更短,您不可能自己实现所有要求的功能,但您仍然希望满足您的客户

      所以你决定允许用户开发他们自己的插件,这样他们就可以&lt;blink'直到奶牛回家。

      要实现这一点,您可能会想出一个如下所示的插件类:

      public class Plugin
      {
      public void Run (PluginHost browser)
      {
      //do stuff here....
      }
      }
      

      但是你怎么能合理地实现那个方法呢?您不可能准确地知道每个可能的未来插件将如何工作

      解决此问题的一种可能方法是将插件定义为一个接口,并让浏览器使用它来引用每个插件,如下所示:

      public interface IPlugin
      {
      void Run(PluginHost browser);
      }
      
      public class PluginHost
      {
      public void RunPlugins (IPlugin[] plugins)
      {
      foreach plugin in plugins
      {
      plugin.Run(this);
      }
      }
      }
      

      请注意,如前所述,IPlugin 接口描述了 Run 方法,但没有指定 Run 是如何工作的,因为这是特定于每个插件的,我们不希望插件主机关注每个单独插件的细节。

      为了演示类和接口之间关系的“can-be-a”方面,我将为下面的插件主机编写一个插件,实现&lt;blink&gt; 标签。

      public class BlinkPlugin: IPlugin
      {
      private void MakeTextBlink(string text)
      {
      //code to make text blink.
      }
      public void Run(PluginHost browser)
      {
      MakeTextBlink(browser.CurrentPage.ParsedHtml);
      }
      }
      

      从这个角度你可以看到插件是在一个名为 BlinkPlugin 的类中定义的,但是因为它也实现了 IPlugin 接口,所以它也可以被称为 IPlugin 对象,就像上面的 PluginHost 类一样,因为它没有知道或关心这个类实际上是什么类型,只是它可以是一个 IPlugin

      我希望这有帮助,我真的不打算这么长。

      【讨论】:

      • 我发现你的答案就是点亮灯泡的那个!
      【解决方案5】:

      我将在下面给你一个例子,但让我从你的一个陈述开始。 “我不知道如何确定何时使用一个”。把它放在边缘。您无需确定何时使用它,而是确定何时不使用它。任何参数(至少对于公共方法),任何(公共)属性(我个人实际上会将列表扩展到其他任何东西)都应该声明为接口而不是特定类。唯一一次我会声明特定类型的东西是在没有合适的接口时。

      我去

      IEnumerable<T> sequence;
      

      当我可以并且几乎永远不会(我能想到的唯一情况是如果我真的需要 ForEach 方法)

      List<T> sequence;
      

      现在是一个例子。假设您正在构建一个可以比较汽车和计算机价格的系统。每个都显示在一个列表中。

      汽车价格数据来自一组网站,计算机价格数据来自一组服务。

      解决方案可能是: 创建一个网页,例如使用数据网格和 IDataRetriever 的依赖注入 (其中 IDataRetriver 是一些接口,使您无需知道数据来自何处(DB、XML、Web 服务或...)或如何获取数据(数据挖掘、内部数据 SQL 查询或读取数据)即可获取数据文件)。

      由于这两个场景我们只有共同的用法,所以超类没有什么意义。但是使用我们的两个类(一个用于汽车,一个用于计算机)的页面需要在两种情况下执行完全相同的操作才能使这成为可能,我们需要告诉页面(编译器)哪些操作是可能的。我们通过一个接口来做到这一点,然后两个类实现该接口。

      使用依赖注入与何时或如何使用接口无关,但我包含它的原因是接口让您的生活更轻松的另一个常见场景。测试。如果您使用注入和接口,您可以在测试时轻松地将生产类替换为测试类。 (这也可能是切换数据存储或强制执行在发布代码中可能很难产生的错误,例如竞争条件)

      【讨论】:

        【解决方案6】:

        我们使用接口(或抽象基类)来实现多态性,这是面向对象编程中非常核心的概念。它允许我们以非常灵活的方式组合行为。如果您还没有阅读过,请阅读Design Patterns - 它包含大量使用接口的示例。

        关于测试替身(例如 Mock 对象),我们使用接口来移除我们目前不想测试的功能,或者在单元测试框架内无法工作的功能。

        特别是在进行 Web 开发时,当代码在 Web 上下文之外执行时,我们依赖的许多服务(例如 HTTP 上下文)不可用,但是如果我们将这些功能隐藏在接口后面,我们可以在测试期间用其他东西替换它。

        【讨论】:

        • 喜欢这个帖子,只是想让你知道你可以在单元测试中使用HttpContext。它确实需要一些前期工作。 (ASP.MVC 没有这个问题)。它基本上是对 HttpContext 的一种破解,所以我同意尽量避免它。但是,如果您需要重构未隐藏 HttpContext 的遗留代码,即使“破解”HttpContext 并不容易,它也可能很有价值。
        • @runefs:我不知道——谢谢你的提示。如果我(或其他人)需要它,您是否有解释什么是 hack 的链接?
        【解决方案7】:

        我的理解是:

        派生是“is-a”关系,例如,狗是动物,牛是动物,但永远不会派生接口,而是实现了接口。

        所以,接口是一种“可能”的关系,例如,A Dog 可以是 Spy Dog,A Dog 可以是 Circus Dog 等。但要实现这一点,狗必须学习一些特定的东西。这在 OO 术语中意味着如果你的类实现了一个接口,它必须能够做一些特定的事情(他们称之为合同)。例如,如果您的类实现了 IEnumerable,这显然意味着您的类具有(必须具有)可以枚举对象的功能。

        因此,从本质上讲,通过接口实现,一个类向其用户公开了一种功能,它可以做某事而不是继承。

        【讨论】:

          【解决方案8】:

          几乎所有关于接口的文章都写完了,让我试一试。

          简单来说,接口就是关联两个或更多类的东西,否则就是不相关的类。

          接口定义契约,确保任何两个或多个类,即使不完全相关,碰巧实现一个公共接口,将包含一组公共操作。

          结合多态性的支持,可以使用接口编写更干净、更动态的代码。

          例如。

          界面生灵

          -- speak() // 说任何IS活着的人都需要定义他们说话的方式

          类狗实现生者

          --speak(){bark;} // 像狗一样说话的实现

          类鸟实现生灵

          --speak(){chirp;}//speak as a bird 的实现

          【讨论】:

          • 这和继承有什么不同?
          • @RadleyAnaya 继承更多的是(状态和行为的)扩展;界面纯粹是关于行为..
          【解决方案9】:
          ICalculator myInterface = new JustSomeClass();
          JustSomeClass myObject = (JustSomeClass) myInterface;
          

          现在您可以在对象上使用两个“接口”。

          我对此也很陌生,但我喜欢将界面视为遥控器上的按钮。使用 ICalculator 界面时,您只能访问界面设计者想要的按钮(功能)。使用 JustSomeClass 对象引用时,您有另一组按钮。但它们都指向同一个对象。

          这样做的原因有很多。对我最有用的是同事之间的沟通。如果他们可以就界面(将被按下的按钮)达成一致,那么一个开发人员可以着手实现按钮的功能,而另一个开发人员可以编写使用这些按钮的代码。

          希望这会有所帮助。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2016-03-10
            • 1970-01-01
            • 2018-12-09
            • 2014-11-21
            • 2013-02-08
            • 2021-02-04
            • 2017-01-13
            相关资源
            最近更新 更多