我正试着在单元测试中站稳脚跟。我目前没有为类编写接口的习惯,除非我预见到一些原因需要在不同的实现中进行交换。好吧,现在我预见到了一个原因:嘲笑。
考虑到我将从少数几个接口发展到数百个接口,我脑海中浮现的第一件事是,我应该把所有这些接口放在哪里?我是应该将它们与所有具体实现混合在一起,还是应该将它们放在一个子文件夹中。例如,控制器接口应该是root/Controllers/interfaces、root/Controlers还是完全是其他类型的?你有什么建议?
在我讨论组织之前:
好吧,现在我预见到了一个原因:嘲笑。
你也可以用类来模拟。子类作为一种选项非常适合mocking,而不是总是生成接口。
接口非常有用,但我建议只有在有理由制作接口的情况下才制作接口。我经常看到当一个类工作正常并且在逻辑方面更合适时创建的接口。您不应该仅仅为了模拟实现而制作"数百个接口"——封装和子类化非常适合这一点。
话虽如此,我通常会将接口和类一起组织起来,因为将相关类型分组到相同的名称空间往往是最有意义的。主要的例外是接口的内部实现——它们可以在任何地方,但我有时会创建一个"内部"文件夹+一个内部命名空间,我专门用于"私有"接口实现(以及其他纯内部实现的类)。这有助于我保持主名称空间的整洁,因此唯一的类型是与API本身相关的主要类型。
这里有一个建议,如果几乎所有的接口都只支持一个类,只需将接口添加到同一名称空间下的类本身的同一文件中。这样一来,您就没有一个单独的接口文件,这可能会使项目变得一团糟,或者只需要一个子文件夹来处理接口。
如果你发现自己使用同一个接口创建了不同的类,我会把接口和类分解到同一个文件夹中,除非它变得完全不规则。但我不认为会发生这种情况,因为我怀疑同一文件夹中是否有数百个类文件。如果是这样的话,应该根据功能对其进行清理和子文件夹,其余部分将自行处理。
对接口的编码远远超出了测试代码的能力。它在代码中创造了灵活性,允许根据产品需求交换不同的实现。
依赖注入是对接口进行编码的另一个很好的理由。
如果我们有一个名为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 */
}
我认为还有一个论点是,要将接口与其实现分开,并在发布周期中以不同的方式对待这些接口,因为这允许并非所有针对相同源编译的组件之间的互操作性。如果对接口进行完全编码,并且接口只能针对主要版本增量而不是次要版本增量进行更改,那么无论次要版本如何,同一主要版本的任何组件组件都应与同一主要版别的任何组件一起工作。通过这种方式,您可以拥有一个缓慢发布周期的库项目,该项目只包含接口、枚举和异常。
这取决于情况。我这样做:如果你必须添加一个依赖的第三方程序集,把具体的版本移到另一个类库中。如果没有,它们可以并排放在同一目录和命名空间中。
我发现,当我的项目中需要数百个接口来隔离依赖关系时,我发现我的设计可能存在问题。当许多接口最终只有一个方法时,情况尤其如此。另一种方法是让对象引发事件,然后将依赖项绑定到这些事件。举个例子,假设您想要模拟持久化数据。一个完全合理的方法是这样做:
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
我发现这比过度使用mock更干净。
我非常不同意接受的答案。
1:虽然技术上是正确的,但你不需要接口,因为你可以选择模拟一个具体的实现,你应该制作一个接口,原因有两个。
您可以使用接口扩展代码,具体实现需要修改,如果您没有扩展,一旦收到更改请求。
1.1:您可以在没有任何实际实现代码的情况下进行TDD(测试驱动开发),只要您只创建要测试的接口即可。这也将迫使您在进行实现之前考虑代码设计。这是一种很好的编码方法。
1.2:
但我建议只有在有理由制作接口的情况下才制作接口。我经常看到当一个类工作正常并且在逻辑方面更合适时创建的接口。
制作界面总是有原因的。因为SOLID的打开/关闭原则表明,您应该以扩展代码为目标,而不是修改代码。这是真的,原因有很多。
1.2.1:用这种方式编写新的单元测试更容易。您只需要将对代码中正在测试的具体实现的依赖作为主题。(在你有一个具体的实现之前,你可以使用一个模拟)
1.2.2:当您有一个具体的实现时,对该具体实现的引用将在整个系统中传播。有了接口,所有引用都将由接口完成,而不是具体实现。这使得扩展成为可能。
1.2.3如果你跟进所有的";叶子;一段代码,遵循原则,如果方法有返回,则该方法不能有副作用,如果方法没有返回,则只能有1个副作用,您还会自动将代码拆分为"S〃;作为SOLID的一部分,这使您的单元测试变得很小,而且很容易维护。
2:如果您想编写干净的代码,接口在技术上是必需的。如果你想遵循SOLID,我不知道没有接口你怎么能做到。
当你打破职责时,你还需要高效地组织你的代码,因为你的代码越解耦,你就有越多的接口和接口的实现。因此,你需要有一个良好的项目管理系统,这样你就不会有"成千上万的接口";随意躺着。
在书、youtube、udemy等网站上都有非常好的指南,这将教会你这一点。(还有一些可怜的,基本上,当你必须为它们付费时,它们的用处会增加)。你必须对主题有足够的了解,以确定免费的是否足够好,如果你计划就此做出商业决策,至少在你这样做之前。