在 C# 中正确使用依赖注入和 YAGNI 混淆
本文关键字:注入 依赖 YAGNI 混淆 | 更新日期: 2023-09-27 18:15:11
我知道你应该依赖抽象而不是具体的实现,但我也知道YAGNI原则。我有时发现自己很难调和这两者。
考虑以下类;
public class Foo
{
public void DoFoo()
{
}
//private foo stuff
}
public class Bar
{
private readonly Foo _foo;
public Bar()
{
_foo = new Foo();
}
}
"Bar"是我感兴趣的类;显然有一个问题,Bar正在实例化Foo的实例,所以让我重构;
public class Bar
{
private readonly Foo _foo;
public Bar(Foo foo)
{
_foo = foo;
}
}
很好,但 Bar 的构造函数仍然依赖于 Foo,一个具体的实现。我没有获得任何东西(我有吗?要解决此问题,我需要使foo成为抽象,这就是我的问题开始的地方。
我找到的每个示例总是(可以理解(演示使用抽象的构造函数注入。我完全赞成防御性编程,但让我们假设除了 Foo 之外,我不需要任何其他实现(测试替身不算在内(。创建一个"IFoo"接口或"FooBase"抽象类肯定违反了YAGNI原则吗?我会为未来可能的情况做一些事情,我以后总是可以这样做,例如
public abstract class Foo
{
public abstract void DoFoo();
//private foo stuff
}
public class Foo1:Foo
{
public override void DoFoo()
{
}
}
这不会破坏 Bar,我甚至可以为接口执行此操作,前提是我放弃了"I"约定(我越来越怀疑(,例如
public interface Foo
{
void DoFoo();
}
public abstract class FooBase:Foo
{
public abstract void DoFoo();
//private foo stuff
}
public class Foo1:FooBase
{
public override void DoFoo()
{
}
}
注入具体实现有什么问题,因为我可以在稍后阶段将其重构为抽象(前提是我给抽象指定与具体实现相同的名称(?
注意:我知道"I"接口命名约定的论点,这不是我问题的重点。我也知道,使 Foo 成为抽象类会在我之前实例化它的任何地方破坏代码,但假设我广泛使用 DI,所以我只需要更改 DI 容器注册,如果我要引入 Foo 的新实现,我可能无论如何都必须这样做。
但 Bar 的构造函数仍然依赖于 Foo,一个具体的实现。我没有获得任何东西(我有吗?
您在这里获得的是,当依赖Foo
本身获得自己的任何依赖关系或需要不同的生活方式时,您可以进行此更改,而无需对Foo
的所有使用者进行彻底的更改。
除了Foo之外,我不需要任何其他实现(测试替身不算在内(
你不能忽略这里的单元测试。正如 Roy Osherove 很久以前解释的那样,您的测试套件是应用程序的另一个(同样重要的(使用者,具有自己的需求。如果添加抽象简化了测试,则创建它不需要其他原因。
创建一个"IFoo"接口或"FooBase"抽象类肯定违反了YAGNI原则吗?
如果您创建此抽象进行测试,则不会违反 YAGNI。在这种情况下,YNI(您需要它(。通过不创建抽象,您可以在生产代码中进行本地优化。这是局部最优而不是全局最优,因为此优化不考虑需要维护的所有其他(同样重要的(代码(即测试代码(。
注入具体实现有什么问题,因为我可以将其重构为抽象
每次看到注入具体实例没有任何错误,尽管 - 如前所述 - 创建抽象可以简化测试。如果它不能简化测试并让消费者对实现进行硬依赖可能是可以的。但请注意,取决于混凝土类型可能有其缺点。例如,在不必对使用者进行更改的情况下,很难用不同的实例(例如拦截器或装饰器(替换它。如果这不是问题,您不妨使用具体类型。
正如 adaam 提到的,做你想做的事并没有错。
您使用什么 DI 容器?如果您使用的是Unity,PRISM有一个很好的示例,说明如何将ViewModels注册为BindableBase(PRISM提供的基类(,除非您有一个实现其他接口的基类。
通常,我有一个扩展BindableBase并实现INotifyDataErrorInfo和其他一些接口的BaseViewModel。然后,当发现模块时,它们将 ViewModels 注册为 BaseViewModel 的类型。
与在 Bar
中实例化它相比,在 Bar
的构造函数中提供Foo
仍然会给你一些东西,即使只是具体的实现。假设你想测试你的Bar
,并且假设你设计Foo
的方式是将所有可能导致单元测试问题的功能都放在虚拟方法中。然后在单元测试中,你从Foo
继承,重写必要的成员,然后将继承的类的实例传递给构造函数Bar
这显然是不可能的,如果你在Bar
本身中实例化Foo
。与 DI 的情况相同 - 您可以将从 Foo
继承的类注册为 DI 容器中的Foo
。