【问题标题】:Correct Use of Dependency Injection and YAGNI confusion in C#在 C# 中正确使用依赖注入和 YAGNI 混淆
【发布时间】:2017-03-10 07:41:18
【问题描述】:

我知道你应该依赖抽象而不是具体的实现,但我也知道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 的新实现。

【问题讨论】:

  • 您可以在 DI 容器中将一个类绑定到自身,而无需创建相应的接口(在 Ninject 中就是这样做的)。在我的脑海里并没有错。视项目规模而定!
  • #adaam - 感谢您的回复,为什么规模可能是一个因素?您是否认为较大的项目更有可能引入新的要求,从而导致新的实施?
  • 为了建立@adaam 提出的观点,您希望由 DI 容器提供具体类型的另一个原因是管理它们的生命周期。如果您使用的对象不包含内部状态,那么将它们作为单例传递并没有明显的错误,尤其是当它们具有昂贵的实例化成本时。
  • @mark_h 是的,抱歉措辞不正确!我指的是项目中代码模块的应用程序数量。如果您正在为单次使用的应用程序设计一个类,并且您知道您不太可能再次使用它,那么连接它就没有什么意义了。我了解到,软件开发既要务实,又要按合同进行设计,这延伸到 DI
  • @mark_h - 是的,这行得通。无论您使用接口还是使用虚拟方法的非密封类似乎都是一个完全不同的讨论。我总是选择接口(即使用接口)并在接口后面的类型上使用继承。这样,消费者仍然只知道合同而不是类型(这可能更难以替换,也可能更难以模拟)。

标签: c# oop dependency-injection


【解决方案1】:

正如亚当所说,做你想做的事没有错。

您使用的是什么 DI 容器?如果您使用 Unity,PRISM 有一个很好的示例,说明如何将您的 ViewModel 注册为 BindableBase(PRISM 提供的基类),除非您有一个实现附加接口的基类。

通常我有一个扩展 BindableBase 并实现 INotifyDataErrorInfo 和其他一些接口的 BaseViewModel。然后,当发现模块时,它们会将 ViewModel 注册为 BaseViewModel 的类型。

【讨论】:

    【解决方案2】:

    与在Bar 中实例化它相比,在Bar 的构造函数中提供Foo 仍然可以为您提供一些东西,即使只是具体的实现。假设您想测试您的Bar,并假设您设计Foo 的方式是所有可能导致单元测试问题的功能都放在虚拟方法中。然后在您的单元测试中,您从Foo 继承,覆盖必要的成员,然后将继承类的实例传递给Bar 构造函数,如果您在Bar 本身内实例化Foo,这显然是不可能的。与 DI 相同 - 您可以将继承自 Foo 的类注册为 DI 容器中的 Foo

    【讨论】:

    • 我可以看到使用注入的具体实现对 Bar 进行单元测试是有问题的,因为我不能轻易地模拟它,但我不喜欢纯粹为了单元目的而公开虚拟方法的想法测试。
    • 我不是说你应该这样做,但你说通过在构造函数中提供 Foo “我没有得到任何东西”,我认为你确实做到了,因为你 可以 i>(不应该)在必要时提供不同的(继承的)实现。
    • 没错,我可以选择重构 Foo 以允许测试(无论是通过虚拟方法、抽象类还是接口)
    【解决方案3】:

    但是 Bar 的构造函数仍然依赖于 Foo,一个具体的实现。我什么都没得到(是吗?)。

    您在这里获得的是,当依赖项 Foo 本身获得任何依赖项或需要不同的生活方式时,您可以进行此更改,而无需对 Foo 的所有使用者进行全面更改。

    我不需要除 Foo 之外的任何其他实现(测试替身不算)

    你不能忽略单元测试。正如很久以前的Roy Osheroveexplained,您的测试套件是您的应用程序的另一个(同样重要的)消费者,有自己的要求。如果添加抽象可以简化测试,那么您应该不需要其他理由来创建它。

    创建一个“IFoo”接口或者一个“FooBase”抽象类肯定违反了YAGNI原则?

    如果您创建此抽象用于测试,您将不会违反 YAGNI。在那种情况下,YNI(你需要它)。通过不创建您在生产代码中本地优化的抽象。这是局部最优而不是全局最优,因为这种优化没有考虑到所有其他(同样重要的)需要维护的代码(即您的测试代码)。

    注入具体实现有什么问题,因为我可以将其重构为抽象

    注入一个具体实例并没有什么错,尽管——正如所说——创建一个抽象可以简化测试。如果它不简化测试并让消费者对实现产生硬依赖可能会很好。但请注意,取决于具体类型可能有其缺点。例如,用不同的实例(例如拦截器或装饰器)替换它变得更加困难,而不必对消费者进行更改。如果这不是问题,您不妨使用具体类型。

    【讨论】:

    • 你关于装饰器的观点是好的!我只是在考虑一种不同的实现,而不是扩展的。
    • @Steven,在您回答的第一个问题中,您的意思是 Foo 而不是 Bar
    • @Steven,我想你可能想改写这句话:“你不会创建 YAGNI,因为你实际上是在测试”,我不明白你的意思。
    • @YacoubMassad:你完全正确。那根本没有任何意义。我改写了。
    猜你喜欢
    • 1970-01-01
    • 2011-12-16
    • 1970-01-01
    • 2014-10-30
    • 2020-05-11
    • 1970-01-01
    • 1970-01-01
    • 2012-05-27
    相关资源
    最近更新 更多