我试图通过单元测试让我的脚湿透。我目前没有为类编写接口的习惯,除非我预见到某些原因需要换成不同的实现。好吧,现在我预见到一个原因:嘲笑。
考虑到我将要从少数几个接口发展到数百个接口,我脑海中闪现的第一件事是,我应该把所有这些接口放在哪里?我只是将它们与所有具体实现混合在一起,还是应该将它们放在一个子文件夹中。例如,控制器接口应该放在 root/Controllers/Interfaces、root/Controllers 中还是完全放在其他位置?你有什么建议?
我试图通过单元测试让我的脚湿透。我目前没有为类编写接口的习惯,除非我预见到某些原因需要换成不同的实现。好吧,现在我预见到一个原因:嘲笑。
考虑到我将要从少数几个接口发展到数百个接口,我脑海中闪现的第一件事是,我应该把所有这些接口放在哪里?我只是将它们与所有具体实现混合在一起,还是应该将它们放在一个子文件夹中。例如,控制器接口应该放在 root/Controllers/Interfaces、root/Controllers 中还是完全放在其他位置?你有什么建议?
在我讨论组织之前:
好吧,现在我预见到一个原因:嘲笑。
你也可以模拟类。子类化非常适合作为一种选项进行模拟,而不是总是制作接口。
接口非常有用 - 但我建议仅在有理由制作接口时才制作接口。我经常看到当一个类可以正常工作并且在逻辑方面更合适时创建的接口。您不应该仅仅为了让自己模拟实现而制作“数百个接口”——封装和子类化对此非常有效。
话虽如此 - 我通常会将我的接口与我的类一起组织,因为将相关类型分组到相同的命名空间往往是最有意义的。主要的例外是接口的内部实现——这些可以在任何地方,但我有时会创建一个“内部”文件夹+一个内部命名空间,专门用于“私有”接口实现(以及其他纯内部实现的类) )。这有助于我保持主命名空间整洁,因此唯一的类型是与 API 本身相关的主要类型。
这里有一个建议,如果你几乎所有的接口都只支持一个类,只需将接口添加到与类本身在同一命名空间下的同一文件中即可。这样,您就没有单独的界面文件,这可能会使项目变得混乱,或者只需要一个子文件夹来存放界面。
如果您发现自己使用相同的接口创建不同的类,我会将接口拆分到与该类相同的文件夹中,除非它变得完全不守规矩。但我认为这不会发生,因为我怀疑您在同一个文件夹中有数百个类文件。如果是这样,则应根据功能对其进行清理和子文件夹,其余的将自行处理。
这取决于。我这样做:如果您必须添加依赖的第 3 方程序集,请将具体版本移到不同的类库中。如果没有,它们可以并排留在同一个目录和命名空间中。
我发现当我的项目中需要数百个接口来隔离依赖时,我发现我的设计中可能存在问题。当许多这些接口最终只有一种方法时尤其如此。这样做的另一种方法是让您的对象引发事件,然后将您的依赖项绑定到这些事件。例如,假设您想模拟持久化数据。一种完全合理的方法是这样做:
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
我发现这比过度使用模拟更干净。
对接口进行编码远远超出了测试代码的能力。它在代码中创造了灵活性,允许根据产品需求换入或换出不同的实现。
依赖注入是编写接口代码的另一个好理由。
如果我们有一个名为 Foo 的对象,它被十个客户使用,现在客户 x 想让 Foo 以不同的方式工作。如果我们已经编码到一个接口(IFoo
),我们只需要实现IFoo
新的需求CustomFoo
。只要我们不改变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
。现在我们可以将接口放在由Foo
和共享的第三个程序集中Bar
。
/* 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 */
}
我认为还有一个论点是维护接口与其实现分开,并在发布周期中以明显不同的方式对待这些接口,因为这允许并非全部针对相同源编译的组件之间的互操作性。如果完全编码到接口并且如果接口只能针对主要版本增量而不是次要版本增量进行更改,那么相同主要版本的任何组件组件都应该与同一主要版本的任何其他组件一起使用,而不管次要版本如何。通过这种方式,您可以拥有一个发布周期较慢的库项目,其中仅包含接口、枚举和异常。