我正在尝试进行单元测试。目前我不习惯为类编写接口,除非我预见到需要替换其他实现。嗯,现在我预见到了一个原因:模拟。
考虑到我要从一些接口增加到可能有数百个接口,首先进入我的脑海的是:这些接口应该放在哪里呢?我是将它们与所有具体实现混合在一起还是将它们放在子文件夹中。例如,控制器接口应该放在根/控制器/接口、根/控制器还是完全不同的位置?你有何建议?
我正在尝试进行单元测试。目前我不习惯为类编写接口,除非我预见到需要替换其他实现。嗯,现在我预见到了一个原因:模拟。
考虑到我要从一些接口增加到可能有数百个接口,首先进入我的脑海的是:这些接口应该放在哪里呢?我是将它们与所有具体实现混合在一起还是将它们放在子文件夹中。例如,控制器接口应该放在根/控制器/接口、根/控制器还是完全不同的位置?你有何建议?
在我讨论组织之前:
好了,现在我预见到一个原因:模拟。
你也可以使用类进行模拟。子类化作为一种选项对于模拟非常有效,而不是总是制作接口。
接口非常有用 - 但我建议只有在需要制作接口时才制作接口。 我经常看到在逻辑上一个类可以很好地工作并且更合适时仍然创建接口。 你不应该需要制作“数百个接口”来允许自己模拟实现 - 封装和子类化非常适用于此。
话虽如此 - 我通常会将我的接口与我的类一起组织,因为将相关类型分组到相同的命名空间中似乎是最明智的选择。主要例外情况是接口的内部实现 - 这些可以放在任何地方,但是我有时会制作一个“Internal”文件夹+一个专门用于“私有”接口实现的Internal命名空间(以及其他纯内部实现的类)。这有助于使主命名空间保持整洁,所包含的类型仅限于与API本身相关的主类型。
这里有一个建议,如果几乎所有的接口都只支持一个类,将接口添加到与该类相同的命名空间下的同一文件中。这样,您就不需要为接口单独创建一个文件,这可能会混乱项目或需要子文件夹来存放接口。
如果您发现自己使用相同接口创建不同的类,我会将接口拆分到与类相同的文件夹中,除非它变得非常混乱。但我不认为会发生这种情况,因为我怀疑您是否在同一个文件夹中有数百个类文件。如果是这样,应该根据功能进行整理和子文件夹分类,剩下的问题就会得到解决。
/* 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
共享。
Original Answer: 最初的回答
/* 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 */
}
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
我认为这比过度使用模拟更加简洁。
这要看情况。我这么做:如果必须添加一个依赖的第三方程序集,将具体的版本移到不同的类库中。如果不需要,它们可以保留在同一目录和命名空间中。
我对被接受的答案有相当大的不同意见。
1:虽然从技术上讲是正确的,但你并不需要一个接口,因为你可以选择模拟一个具体的实现,但你应该出于两个原因而创建一个接口。
你可以通过接口扩展你的代码,具体的实现需要修改,如果你没有扩展,一旦你得到一个变更请求,就会很麻烦。
1.1: 只要你创建接口来测试,你就可以进行TDD(测试驱动开发),而不需要任何实际的实现代码。这也会迫使你在实现之前考虑代码设计。这是一种优秀的编码方法。
1.2:
我建议只有在有理由的情况下才制作接口。我经常看到创建接口时,类可以很好地工作并且在逻辑上更合适。2: 如果你想写出干净的代码,技术上是需要接口的。如果你想遵循SOLID原则,我不知道你怎么能没有接口来实现。
当你分解职责时,你还需要高效地组织你的代码,因为代码解耦得越多,你就会有越多的接口和接口的实现。因此,你需要一个良好的项目管理系统,这样你就不会随意地拥有“成百上千个接口”。
在书籍、YouTube、Udemy等地方有很多非常好的指南可以教你这些知识(当然也有一些质量较差的,通常情况下,付费的指南更有用)。在做出商业决策之前,你必须对主题有足够的了解,以判断免费的指南是否足够好。
callvirt
。我同意它有其适用的时间和场合,但模拟往往会产生副作用(不幸的是)。 - Reed Copsey