我正在为一个项目(可能还有类似的项目)在 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”类,它可以创建我所有的包装器对象。但这对我来说感觉很尴尬,原因有两个:
DirectoryFacade 的所有实例现在都需要传递一个 FacadeFactory,以便它们可以动态创建 FileFacade。这只是感觉有点不对...
对于必须使用工厂来创建 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');
现在,从客户的角度来看,这太可怕了。如前所述,如果使用解析器或关键字工厂的通常内部实现细节发生变化,它也会中断。
当然,我们可以再次编写一些额外的工厂和外观来为客户端提供默认实现,但这真的有意义吗?它提供什么好处?解析器和关键字工厂很可能只有一种实现。此外,它们只会在“配置”类中使用。他们在不同班级的唯一原因是每个班级都有明确定义的职责......