C# 与手动依赖注入抗争

C# 与手动依赖注入抗争,c#,dependency-injection,C#,Dependency Injection,我正在为一个项目(可能还有类似的项目)在C#中开发一个小型(ish)支持库。我读过很多关于依赖项的构造函数注入的书,虽然我理解其背后的基本动机,这在处理某些类型的依赖项时非常有意义,但我经常遇到这样的情况,即它似乎只会让事情变得更困难,而不会带来太多好处 我想举两个例子,并非常感谢任何关于如何“适当”处理这些问题的建议。在这一点上,我还想指出,目前我不想使用任何DI容器,因为我希望库在没有DI容器的情况下可用 示例1:动态创建的对象 大多数与DI相关的文章强调,对象图的创建发生在合成根。但是,并

我正在为一个项目(可能还有类似的项目)在C#中开发一个小型(ish)支持库。我读过很多关于依赖项的构造函数注入的书,虽然我理解其背后的基本动机,这在处理某些类型的依赖项时非常有意义,但我经常遇到这样的情况,即它似乎只会让事情变得更困难,而不会带来太多好处

我想举两个例子,并非常感谢任何关于如何“适当”处理这些问题的建议。在这一点上,我还想指出,目前我不想使用任何DI容器,因为我希望库在没有DI容器的情况下可用

示例1:动态创建的对象

大多数与DI相关的文章强调,对象图的创建发生在合成根。但是,并非所有对象都可以最初创建。对此的标准答案是将工厂用于短期对象。这当然有效,但在某些情况下会感到非常尴尬

假设我想围绕第三方库中的对象创建一系列facade类,其中一个对象可能返回另一种类型的对象,我想包装这两种类型。作为一个(虚构的)示例,让我们使用一个“目录”类,该类具有一个方法“GETFILE()”,它返回一个“文件”类的实例。立面的非DI版本可能如下所示:

public DirectoryFacade(string path)
{
    m_directory = new Directory(path);
}
public IEnumerable<FileFacade> GetFiles()
{
    foreach(File file in m_directory.GetFiles())
    {
        yield return new FileFacade(file);
    }
}
在内部,“配置”类可以将字符串解析为某种数据结构。为此,它可以使用一个名为“ConfigurationParser”的类。更进一步说:解析器本身可以输出IKeyword对象流。IKeyword接口的具体实现取决于单个关键字的类型。因此关键字可能由“关键字工厂”生成。。。“配置”类的客户机对所有这些都毫无兴趣。事实上,我应该能够在不影响客户机代码的情况下将所有这些更改为其他内容。然而,对于DI,您需要执行以下操作:

KeywordFactory factory = new KeywordFactory();
ConfigurationParser parser = new ConfigurationParser(factory);
Configuration config = new Configuration(parser);
config.ConfigurationString = configString; //NOTE: this could be a constructor param...
config.SetValueForKey('foo','bar');
现在,从客户的角度来看,这是可怕的。如前所述,如果使用解析器或关键字工厂的正常内部实现细节发生变化,那么它也会中断


当然,我们可以再次编写一些额外的工厂和外观来为客户机提供默认实现,但这真的有意义吗?它提供了什么好处?解析器和关键字工厂很可能只有一个实现。此外,它们将仅在“配置”类中使用。他们属于不同类别的唯一原因是,每个类别都有明确定义的责任…

我认为您将工厂和立面的设计过度复杂化了,这是您的问题,而不是DI。注入更简单的对象,让它们为类做一些工作

例1:

使用Di容器,您只需

var fileFacade = // Get me a FileFacade and sort out all it's dependencies for me.
你过度使用立面和工厂模式,过度设计。模式的存在并不意味着你必须在任何地方使用它(DI也是如此)。你想在这里实现什么?要一些文件吗?注入执行此操作的接口。保持简单

例2:

这是一个DI容器。前三行或四行替换为一行。
配置
类的新实例的创建会自动包含其依赖项

KeywordFactory factory = new KeywordFactory();
ConfigurationParser parser = new ConfigurationParser(factory);
Configuration config = new Configuration(parser);
config.ConfigurationString = configString; //NOTE: this could be a constructor param...
config.SetValueForKey('foo','bar');
因此,你认为这对客户来说很可怕的论点已经不存在了

Configuration config = // Get me a new Configuration instance along with dependencies and ctor arguments.
config.SetValueForKey('foo','bar');
回答你的一般问题,你是对的,DI不是到处都可以使用的。只要在有意义的时候使用它。我主要并且完全地使用它来促进单元测试

现在使用DI时,DirectoryFacade本身不应该创建任何FileFacades

这就是你错的地方。在应用程序的任何地方使用
new
关键字都不是DI阻止的意图。DI是关于松耦合的。你不应该试图抽象那些不需要抽象的东西。您应该区分服务(应用程序逻辑)和其他任何东西(DTO、消息、实体、命令、异常、原语等)。这是两者之间的重要区别

在这种情况下,
FileFacades
是新事物。它们的寿命很短,只包含数据,没有自己的依赖关系,几乎没有逻辑需要测试,并且主要作为瘦代理存在于实际实现中。注入它们意味着您希望它们被不同的实现(例如测试)所取代。但如果从来都不是这样的话,就不用费心注射了。在这种情况下,让
DirectoryFacade
创建
FileFacade
实例似乎非常合理。这样就完全不需要使用
FileFacadeFactory

因此关键字可能由“关键字工厂”生成

这句话又是出于同样的误解。关键字可能只是数据包。这里没有要抽象的逻辑,也没有理由不让
ConfigurationParser
更新
关键字
实例本身

你可以再写一些额外的工厂和门面给我 为客户机提供默认实现,但是 真的有道理吗?它提供了什么好处

从另一边看。如果您不这样构造代码,您如何对代码进行单元测试

在我看来,作为一名框架作者,您有两项责任。1.使代码正确工作(通过单元测试)。2.为您的客户设计一个易于使用的API

如果所有这些类都是实现细节,并且不打算让客户机更改,请确保它们保留在实现细节中。例如,将这些类设置为内部类,并对lib的根类型使用poor-man依赖注入
Configuration config = // Get me a new Configuration instance along with dependencies and ctor arguments.
config.SetValueForKey('foo','bar');
public class MyPublicAPI
{
    public MyPublicAPI()
        : this(new Configuration(
            new ConfigurationParser()))
    {
    }

    internal MyPublicAPI(IConfiguration configuration)
    {
        this.configuration = configuration;
    }
}