我正试图通过单元测试来弄湿我的脚。我目前不习惯为类编写接口,除非我预见到某些原因我需要交换不同的实现。好吧,现在我预见到一个原因:嘲笑。
鉴于我将从几个接口转到可能数百个接口,首先突然出现的是,我应该把所有这些接口放在哪里?我只是将它们与所有具体实现混合在一起,还是应该将它们放在子文件夹中。例如,控制器接口应该在根/控制器/接口,根/控制器或其他完全?你有什么建议吗?
答案 0 :(得分:18)
在我讨论组织之前:
嗯,现在我预见到一个原因:嘲笑。
您也可以使用类进行模拟。子类化非常适合作为选项进行模拟,而不是总是创建接口。
接口非常有用 - 但如果有理由建立接口,我建议只创建一个接口。我经常看到当一个类工作正常并且在逻辑方面更合适时创建的接口。您不应该仅仅为了让自己模拟实现而制作“数百个接口” - 封装和子类化对此非常有效。
话虽如此 - 我通常会将我的界面与我的类一起组织,因为将相关类型分组到相同的命名空间往往最有意义。主要的例外是接口的内部实现 - 这些可以在任何地方,但我有时会创建一个“内部”文件夹+一个内部命名空间,我专门用于“私有”接口实现(以及纯粹内部实现的其他类) )。这有助于我保持主命名空间的整洁,因此唯一的类型是与API本身相关的主要类型。
答案 1 :(得分:8)
这是一个建议,如果几乎所有接口都只支持一个类,只需将接口添加到与同一名称空间下的类本身相同的文件中。这样你就没有一个单独的接口文件,它可能会使项目变得混乱,或者需要一个仅用于接口的子文件夹。
如果您发现自己使用相同的界面创建不同的类,我会将界面分解为与该类相同的文件夹,除非它变得完全不守规矩。但我不认为这会发生,因为我怀疑你在同一个文件夹中有数百个类文件。如果是这样,那应该根据功能进行清理和子文件夹,其余部分将自行处理。
答案 2 :(得分:1)
这取决于。我这样做:如果你必须添加一个从属的第三方程序集,将具体版本移到另一个类库。如果没有,他们可以在同一目录和命名空间中并排。
答案 3 :(得分:0)
答案 4 :(得分:0)
我发现当我在项目中需要数百个接口来隔离依赖项时,我发现我的设计中可能存在问题。当许多这些接口最终只有一种方法时尤其如此。执行此操作的替代方法是让对象引发事件,然后将依赖项绑定到这些事件。举个例子,假设您要模拟保存数据。一个完全合理的方法是这样做:
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
我发现这比过度使用模拟更清洁。
答案 5 :(得分:0)
我没有足够的声誉来发表评论,因此,如果我想提出一点,我必须回答。 (去搞清楚!) 接口的编码远远超出了能够测试代码的范围。它在代码中创造了灵活性,允许根据产品要求换入或换出不同的实现。
依赖注入是编写接口代码的另一个很好的理由。
如果我们有一个名为Foo的对象,该对象有10个客户使用,现在客户x希望Foo以另一种方式工作。如果我们已将代码编码为接口(IFoo),则只需将IFoo实施到CustomFoo中的新要求即可。只要我们不更改IFoo,就不需要太多。客户x可以使用新的CustomFoo,其他客户可以继续使用旧的Foo,并且不需要进行其他任何代码更改。
不过,我真正想指出的一点是,接口可以帮助消除循环引用。如果我们有一个对象X依赖于对象Y,而对象Y依赖于对象X。我们有两个选择1.对象x和y必须在同一组件中,或者2.我们必须找到某种方式打破循环参考。我们可以通过sharin ginterface而不是共享实现来实现。 / *单片组装* / 公共课 { IEnumerable _bars; 公共无效Qux() { foreach(_bars中的var bar) { bar.Baz(); }
}
/* rest of the implmentation of Foo */
}
public class Bar
{
Foo _parent;
public void Baz()
{
/* do something here */
}
/* rest of the implmentation of Bar */
}
如果foo和bar具有完全不同的用法和依赖关系,我们可能不希望它们在同一程序集中,尤其是在该程序集已经很大的情况下。
为此,我们可以在一个类(例如Foo)上创建一个接口,并引用Bar中的接口。现在,我们可以将接口放入Foo和Bar共享的第三个程序集中。 / *共享的Foo汇编* / 公共接口IFoo { 无效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 implmentation of Foo */
}
/* Bar assembly */
public class Bar:IBar
{
IFoo _parent;
/* rest of the implmentation of Bar */
public void Baz()
{
/* do something here */
}
我认为还有一个论点,就是将接口与其实现分开进行维护,并在发布周期中对它们进行明显的区别对待,因为这允许在并非全部针对同一源进行编译的组件之间实现互操作性。如果对接口进行了完全编码,并且只能针对主要版本增量而不是次要版本增量来更改接口,则相同的主要版本的任何组件组件都应与相同的主要版本的任何其他组件一起工作,而不管次要版本如何。 这样,您可以使一个库项目的发布周期很慢,其中仅包含接口,枚举和异常。