苦苦挣扎于手动依赖注入

时间:2013-08-19 08:18:45

标签: c# dependency-injection

我正在C#中为一个项目(以及可能类似的项目)开发一个小型(ish)支持库。我已经阅读了很多关于构造函数注入依赖项的知识,虽然我理解它背后的基本动机,这在处理某些类型的依赖项时非常有意义,但我经常会遇到这样的情况:它似乎只是在没有提供太多好处的情况下使事情变得更难。

我想展示两个例子,非常感谢任何关于如何“正确”处理它们的建议。我还想在这一点上注意,现在我不想使用任何DI容器,因为我希望这个库可以在没有它的情况下使用...

示例1:动态创建的对象

大多数DI相关文章强调对象图的创建发生在组合根。但是,最初不能创建所有对象。对此的标准答案是将工厂用于短寿命对象。这当然有效,但在某些情况下会觉得很尴尬...

假设我想在第三方库中创建一系列围绕对象的外观类,其中一个对象可能返回另一种类型的对象,并且我想要包装它们。作为一个(组成)示例,让我们采用一个'Directory'类,它有一个方法'GetFiles()',它返回'File'类的实例。外墙的非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);
    }
}

相当简单直截了当。现在使用DI,DirectoryFacade本身不应该创建任何FileFacades(也不应该创建包装目录,但这是较小的问题......)。同样,通常提出的解决方案是建立一个工厂,例如'FacadeFactory'类,可以创建我的所有包装器对象。但这让我觉得很尴尬有两个原因:

  1. 现在需要将所有DirectoryFacade实例传递给FacadeFactory,以便他们可以动态创建FileFacades。这只是感觉不对......

  2. 图书馆的用户必须使用工厂创建DirectoryFacades而不是直接实现它们,这可能不直观......

  3. 现在,可能有人可以使用默认构造函数或静态工厂方法来提供默认实现,但是这些似乎被避开了,通常引用DI容器问题...而且,使用静态工厂方法到处都有点落差在我的观点中,在非直观的范畴内。

    示例2:内部帮助程序类

    我最近发现问题的另一个案例是,你将复杂类的实现分解为各个子组件以遵循单一责任原则,但是你的类的用户不应该知道这一点。 ..

    例如,使用一个类来促进与复杂的键值对配置字符串的交互,提供诸如'GetValueByKey()'和'SetValueForKey()'等方法。

    在正常用例中,客户端代码可能只想执行以下操作:

    Configuration config = new Configuration(configString);
    config.SetValueForKey('foo','bar');
    ...
    

    在内部,您的“Configuration”类可以将字符串解析为某种数据结构。为此,它可以使用一个名为“ConfigurationParser”的类。并且更进一步:解析器本身可以输出IKeyword对象流。 IKeyword接口的具体实现取决于各个关键字类型。所以关键字可能由'KeywordFactory'生成......所有这些对'Configuration'类的客户来说完全没有意义。事实上,我应该能够在不影响客户端代码的情况下将所有这些更改为其他内容。然而,对于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');
    

    现在,从客户的角度来看,这太可怕了。如前所述,如果使用解析器或关键字工厂的正常内部实现细节发生变化,它也会破坏。

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

2 个答案:

答案 0 :(得分:1)

我认为你使用factoriesfacades使设计过于复杂,这是你的问题,而不是DI。注入更简单的对象,让它们为你的班级做一些工作。

  

示例1:

使用Di容器,您只需

var fileFacade = // Get me a FileFacade and sort out all it's dependencies for me.

您正在使用立面和工厂模式以及过度设计。仅仅因为存在一种模式并不意味着你必须在任何地方使用它(DI也是如此)。你想在这里实现什么?获取一些文件?注入执行此操作的界面。保持简单。

  

示例2:

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

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不会随处使用。只要有意义就可以使用它。我主要和完全,用它来促进单元测试。

答案 1 :(得分:1)

  
    

现在使用DI,DirectoryFacade不应该自己创建任何FileFacades

  

这是你出错的地方。 DI决不会阻止在您的应用程序中的任何位置使用new关键字。 DI是松耦合的。而且你不应该试图抽象那些不必抽象的东西。您应该区分服务(应用程序逻辑)和其他任何东西(DTO,消息,实体,命令,异常,基元等)。这是newables and injectables之间的重要区别。

在这种情况下,您的FileFacades是新手。它们是短暂的,只包含数据,没有自己的依赖,几乎没有逻辑可以测试,并且主要作为实际实现的瘦代理存在。注入它们意味着您有兴趣将它们替换为不同的实现(例如用于测试)。但如果情况从未如此,请不要费心去注射它们。在这种情况下,让DirectoryFacade创建FileFacade实例似乎是完全合理的。这完全消除了拥有FileFacadeFactory

的需要
  
    

因此关键字可能由'KeywordFactory'生成......

  

这句话再次来自同样的误解。关键字可能只是数据包。这里没有抽象的逻辑,没有理由ConfigurationParser不应该新建Keyword个实例本身。

  
    人们可以再写一些额外的工厂和外墙     为客户端提供默认实现,但这样做     真有意义吗?它提供了哪些好处?

  

从另一边看。如果您没有像这样构建代码,那么如何对代码进行单元测试?

在我看来,你 - 作为框架作家 - 有两个责任。 1.使代码正常工作(通过单元测试它的地狱)。 2.设计一个易于为您的客户使用的API。

如果所有这些类都是实现细节而不是客户端要更改,请确保它们保留实现细节。例如,将这些类放在内部,并在库的根类型上使用穷人的依赖注入。对于穷人的DI,你有一个公共默认构造函数,它调用另一个接受所有依赖项的构造函数。这是一个例子:

public class MyPublicAPI
{
    public MyPublicAPI()
        : this(new Configuration(
            new ConfigurationParser()))
    {
    }

    internal MyPublicAPI(IConfiguration configuration)
    {
        this.configuration = configuration;
    }
}
许多人不鼓励穷人的DI,但这种建议通常是在业务线应用程序的背景下给出的。如果你编写一个可重用的库,拥有一个干净的API是最重要的,并且你的用户确实不应该在运行一个常见用例时必须实现多个类(当然,如果用户想要做更复杂的事情,他们可能需要创建更多的类)。此外,您的用户可能不会使用DI容器,或者甚至在他们这样做时,您不应强迫他们必须注册所有库类型。这不是非常友好且容易出错,因为每次添加新类型时,都会破坏其DI配置。

穷人的DI并不总是一个选项,因为您对使用过的依赖项进行了硬编码。如果您希望为用户提供更大的灵活性,您仍可能不得不依赖于创建用户可以覆盖的工厂。这完全取决于您的框架的复杂程度以及为用户提供的灵活性。

另一种选择是将集成DLL与您的库一起发布,以便将库与公共DI容器集成。所以经常看到这个,但你不应该这样做,除非用户真的需要这种灵活性。你还在打开你的图书馆,这有一些缺点。首先,您更难以使用您的框架,并且API更大。这也使您更难以更改框架。更改库的公共API可能会导致重大更改。