3

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

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

示例 1:动态创建的对象

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

假设我想在 3rd 方库中的对象周围创建一系列外观类,其中一个对象可能返回另一种类型的对象,我想包装两者。作为一个(组成的)示例,让我们以一个“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,以便它们可以动态创建 FileFacade。这只是感觉有点不对...

  2. 对于必须使用工厂来创建 DirectoryFacades 而不仅仅是能够直接实例化它们的库的用户来说,这可能是不直观的......

现在,可能有人可以使用默认构造函数或静态工厂方法来提供默认实现,但那些似乎被回避,通常引用 DI 容器问题......此外,在任何地方使用静态工厂方法也有点不直观我认为的类别。

示例 2:内部帮助程序类

我最近发现有问题的另一种情况是,您可以将复杂类的实现分解为多个子组件以遵循单一职责原则,但是您的类的用户不需要知道...

例如,采用一个便于与键值对的复杂配置字符串进行交互的类,该类提供诸如“GetValueByKey()”和“SetValueForKey()”等方法。

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

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

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

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

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

4

2 回答 2

2

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

这就是你出错的地方。DI 的意图绝不是阻止new在应用程序的任何地方使用关键字。DI 是关于松散耦合的。而且您不应该尝试抽象不必抽象的事物。您应该区分服务(应用程序逻辑)和其他任何东西(DTO、消息、实体、命令、异常、原语等)。这是newables和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 可能会导致重大更改。

于 2013-08-19T12:50:09.420 回答
1

我认为您使用factoriesand使设计过于复杂facades,这是您的问题,而不是 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 并非无处不在。只需在有意义的时候使用它。我主要和完全地使用它来促进单元测试。

于 2013-08-19T08:34:31.463 回答