在 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 的新实现,我可能无论如何都必须这样做。

在 C# 中正确使用依赖注入和 YAGNI 混淆

但 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