【问题标题】:Where do I put all these interfaces?我将所有这些接口放在哪里?
【发布时间】:2011-09-15 23:53:44
【问题描述】:

我正在尝试进行单元测试。我目前没有为类编写接口的习惯,除非我预见到某些原因需要换成不同的实现。好吧,现在我预见到了一个原因:嘲笑。

考虑到我要从少数几个接口变成数百个接口,我脑海中闪现的第一件事是,我应该把所有这些接口放在哪里?我只是将它们与所有具体实现混合在一起,还是应该将它们放在一个子文件夹中。例如,控制器接口应该放在 root/Controllers/Interfaces、root/Controllers 中还是完全放在其他位置?你有什么建议?

【问题讨论】:

    标签: c# unit-testing interface dependency-injection project-organization


    【解决方案1】:

    在我讨论组织之前:

    好吧,现在我预见了一个原因:嘲弄。

    您也可以使用类进行模拟。子类化非常适合作为一种选项进行模拟,而不是总是制作接口。

    接口非常有用 - 但我建议仅在有理由制作接口时才制作接口。我经常看到当一个类可以正常工作并且在逻辑方面更合适时创建的接口。您不应该仅仅为了让自己模拟实现而制作“数百个接口”——封装和子类化对此非常有效。

    话虽如此 - 我通常会将我的接口与我的类一起组织,因为将相关类型分组到相同的命名空间往往是最有意义的。主要的例外是接口的内部实现——这些可以在任何地方,但我有时会创建一个“内部”文件夹+一个内部命名空间,专门用于“私有”接口实现(以及其他纯内部实现的类) )。这有助于我保持主命名空间整洁,因此唯一的类型是与 API 本身相关的主要类型。

    【讨论】:

    • 谢谢,里德,+1。我很欣赏关于并不总是需要一个接口来模拟的评论。我想我对需要将所有公共方法标记为virtual 的想法感到有点不舒服(因为这似乎是起订量所需要的)。我在这里阅读了一些答案,给我的印象是过于宽松地使用virtual 可能是一个坏主意。但也许这不是一个大问题。你能对此发表评论吗?
    • @DanM:如果你使用一个界面,相反,你实际上会做同样的事情。任何接口都会强制使用callvirt。我同意它有时间和地点,但嘲笑往往会产生副作用(不幸的是)。
    • 忽略 callvirt 问题,在任何地方使用 virtual 是一个设计问题,因为它可能允许类修改其他类的现有行为。使用接口,您实际上并没有这些,因为没有关联的行为。
    • @Matt H:是的——虽然如果你在传递一个接口,你实际上也允许注入类来替换行为。如果你想模拟整个班级,你别无选择,只能在某种程度上允许这样做。
    • 这个建议的替代方案(我不同意)是在组合根中声明所有接口(如果您使用 DI)和“服务提供者”。因为组合根,将设置所有必需的依赖项,并且如果您想跨项目重用接口,这更有意义
    【解决方案2】:

    这里有个建议,如果你几乎所有的接口都只支持一个类,只需将接口添加到与类本身在同一命名空间下的同一文件中即可。这样一来,您就没有单独的界面文件,这可能会使项目变得混乱,或者只需要一个子文件夹来存放界面。

    如果您发现自己使用相同的接口创建不同的类,我会将接口拆分到与该类相同的文件夹中,除非它变得完全不守规矩。但我认为这不会发生,因为我怀疑您在同一个文件夹中有数百个类文件。如果是这样,则应根据功能对其进行清理和子文件夹,其余的将自行处理。

    【讨论】:

    • 10 年过去了,我正坐在一个项目前面,其中每个类都有一个接口(出于其他答案中列出的各种充分理由)。它们位于不同文件中的唯一原因是因为 linting 规则...
    【解决方案3】:

    这取决于。我这样做:如果您必须添加依赖的第 3 方程序集,请将具体版本移到不同的类库中。如果没有,它们可以并排留在同一个目录和命名空间中。

    【讨论】:

      【解决方案4】:

      我发现当我的项目中需要数百个接口来隔离依赖时,我发现我的设计中可能存在问题。当许多这些接口最终只有一种方法时尤其如此。这样做的另一种方法是让您的对象引发事件,然后将您的依赖项绑定到这些事件。例如,假设您想模拟持久化数据。一种完全合理的方法是这样做:

      public interface IDataPersistor
      {
          void PersistData(Data data);
      }
      
      public class Foo
      {
          private IDataPersistor Persistor { get; set; }
          public Foo(IDataPersistor persistor)
          {
              Persistor = persistor;
          }
      
          // somewhere in the implementation we call Persistor.PersistData(data);
      
      }
      

      不使用接口或模拟的另一种方法是这样做:

      public class Foo
      {
          public event EventHandler<PersistDataEventArgs> OnPersistData;
      
          // somewhere in the implementation we call OnPersistData(this, new PersistDataEventArgs(data))
      }
      

      然后,在我们的测试中,您可以这样做而不是创建模拟:

      Foo foo = new Foo();
      foo.OnPersistData += (sender, e) => { // do what your mock would do here };
      
      // finish your test
      

      我发现这比过度使用模拟更干净。

      【讨论】:

      • 好主意,但是通过 Nullable 编译器检查,您会发现您必须在构造函数中注入该处理程序,或者使其可为空(这对逻辑可能没有意义),从而使您重新进入需要新界面的同一条船。
      【解决方案5】:

      对接口进行编码远远超出了测试代码的能力。它在代码中创造了灵活性,允许根据产品需求换入或换出不同的实现。

      依赖注入是编写接口代码的另一个好理由。

      如果我们有一个名为 Foo 的对象,它被十个客户使用,现在客户 x 希望让 Foo 以不同的方式工作。如果我们已经编码到一个接口(@98​​7654321@),我们只需要根据CustomFoo 中的新要求实现IFoo。只要我们不更改IFoo,就不需要太多。客户 x 可以使用新的CustomFoo,其他客户可以继续使用旧的 Foo,并且需要进行一些其他代码更改来适应。

      但我真正想说的是,接口可以帮助消除循环引用。如果我们有一个对象 X 依赖于对象 Y 并且对象 Y 依赖于对象 X。我们有两个选择 1. 对象 x 和 y 必须在同一个程序集中,或者 2. 我们必须找到一些方法打破循环引用。我们可以通过共享接口而不是共享实现来做到这一点。

      /* Monolithic assembly */
      public class Foo
      {
          IEnumerable <Bar> _bars;
          public void Qux()
          {
             foreach (var bar in _bars)
             {
                 bar.Baz();
             }
      
          }
          /* rest of the implmentation of Foo */
      }
      
      public class Bar
      {
          Foo _parent;
          public void Baz()
          {
          /* do something here */
          }
          /* rest of the implementation of Bar */
      }
      

      如果 foo 和 bar 具有完全不同的用途和依赖关系,我们可能不希望它们在同一个程序集中,特别是如果该程序集已经很大。

      为此,我们可以在其中一个类上创建一个接口,例如Foo,并引用Bar 中的接口。现在我们可以将接口放在FooBar 共享的第三个程序集中。

      /* Shared Foo Assembly */
      public interface IFoo
      {
          void Qux();
      }
      
      /* Shared Bar Assembly (could be the same as the Shared Foo assembly in some cases) */
      public interface IBar
      {
          void Baz();
      }
      /* Foo Assembly */
       public class Foo:IFoo
      {
          IEnumerable <IBar> _bars;
          public void Qux()
          {
             foreach (var bar in _bars)
             {
                 bar.Baz();
             }
      
          }
          /* rest of the implementation of Foo */
      }
      /* Bar assembly */
      public class Bar:IBar
      {
          IFoo _parent;
          /* rest of the implementation of Bar */
          public void Baz()
          {
              /* do something here */
      }
      

      我认为还有一个论点是维护接口与其实现分开,并在发布周期中以明显不同的方式对待它们,因为这允许并非全部针对相同源编译的组件之间的互操作性。如果完全编码到接口并且如果接口只能针对主要版本增量而不是次要版本增量进行更改,那么相同主要版本的任何组件组件都应该与同一主要版本的任何其他组件一起使用,而不管次要版本如何。 通过这种方式,您可以拥有一个发布周期较慢的库项目,其中仅包含接口、枚举和异常。

      【讨论】:

        猜你喜欢
        • 2011-11-13
        • 1970-01-01
        • 2011-01-26
        • 1970-01-01
        • 2015-08-21
        • 1970-01-01
        • 1970-01-01
        • 2011-02-18
        • 2016-05-17
        相关资源
        最近更新 更多