【.Net依赖注入】DI container失败的原因是出现了“复杂”的对象图

英文原文:

http://yacoubsoftware.blogspot.com/2015/08/why-di-containers-fail-with-complex.html

译者前述:
本文重点比较了DI container和Pure DI的优缺点。

 在面向对象编程中,Solid是一组原则,它允许我们创建灵活的软件,在未来易于维护和扩展。如果我们使用这些原则,我们的代码库中最终会有很多小类。这些类中的每一个都只有一个职责,但可以与其他类协作。

 我们使用依赖注入(Dependency Injection)来创建松散耦合的类。类并不直接依赖于其他类,而是依赖于抽象。一个类声明它依赖于特定的抽象,当创建这样一个类的对象时,第三方将注入这种抽象的具体实现。
 这个第三方通常是一些驻留在应用程序入口点中的代码,称为复合根。这段代码将创建一个表示应用程序的对象图。

创建此类对象图的两种方式是:

  1. 使用依赖注入(DI)容器(也称为控制反转(IOC)容器)。
  2. Pure DI

 在本文中,我将讨论使用DI containers的一个问题,并展示使用pure DI可能是一个更好的解决方案。
 让我们从使用DI容器创建对象图的示例开始。请考虑以下UML类关系图:
在这里插入图片描述
 在这里,OrderProcessor类依赖于ICurrencyConverter和IEmailSender接口。
 EmailSender类需要smtp server address才能运行。此类信息通过其构造函数提供给类。同样,CurrencyConverter类需要货币转换Web服务URL才能运行。这样的URL通过构造函数提供给类。
 OrderProcessor类构造函数需要IEmailSender和ICurrencyConverter。
 我们可以使用DI容器将每个接口映射到实现它的相应类。在这样的映射之后,我们可以使用容器创建任意数量的对象。以下代码使用Unity DI Container(这是Microsoft创建的DI容器)演示了这一点:

UnityContainer container = new UnityContainer();

container.RegisterType<IEmailSender, EmailSender>();

container.RegisterType<ICurrencyConverter, CurrencyConverter>();

container.RegisterType<IOrderProcessor, OrderProcessor>();

var processor = container.Resolve<IOrderProcessor>(
    new ParameterOverride("url", "http://service.currency.lab"),
    new ParameterOverride("smtp_server", "smtp.lab.lab"));

 在这里,我们将每个接口映射到实现它的类。在此之后,我们要求容器为我们构建一个订单处理器。我们提供任何必需的设置,例如货币转换Web服务URL和电子邮件服务器地址。除此之外,容器知道如何使用所谓的自动连接来构造对象图。
 容器首先在注册的映射中查找IOrderProcessor接口。它会发现它被映射到OrderProcessor类。查看此类,容器发现此类的构造函数需要IEmailSender和ICurrencyConverter。现在容器将尝试解析这种依赖关系,它会查看其注册映射。它将发现IEmailSender接口映射到EmailSender,ICurrencyConverter接口映射到CurrencyConverter类。容器将尝试创建一个EmailSender类。查看它的构造函数,它会发现它需要一个字符串“url”。因为我们在调用Resolve方法时提供了这样的“url”,所以容器将能够构造EmailSender。CurrencyConverter类也是如此。创建这些依赖项之后,容器现在就可以创建OrderProcessor类了。无论对象图的大小或深度如何,此过程都会起作用。
 在pure DI中,我们手动创建对象图。下面是我们如何使用pure DI创建相同的对象图:

var processor = new OrderProcessor(
                new EmailSender("smtp.lab.lab"),
                new CurrencyConverter("http://service.currency.lab"));

 使用DI容器的好处来了:设想您有一个更大的对象图。假设除了电子邮件发送者服务和货币兑换服务之外,您还有订单存储服务(或订单存储库)、SMS发送服务、信用卡处理服务、订单排队服务、客户忠诚度管理服务、日志服务、安全相关服务和许多其他服务。假设有20个这样的服务,其中一些服务是相互依赖的。假设您有大约20个应用程序级别的类(如ASP.NET控制器),它们依赖于这些服务才能工作。手动构建这些类需要大量代码,因此维护成本更高。
 为了便于讨论,我将定义以下内容:

  1. 简单对象图:具有以下两个属性的任意大小和任意深度的对象图:
    a. 对于任何接口(或抽象),在对象图中最多使用一个类来实现此类接口。
    b. 对于任何类,在对象图中最多使用一个实例(或具有完全相同的构造参数参数的多个实例)。此单个实例可以在图形中多次使用。
  2. 复杂对象图:任何其他对象图。
     前面的对象图是简单对象图的一个示例。
     请注意,我在这里使用的术语“简单”和“复杂”具有特殊的含义。我选择这些术语是因为我想不出更好的术语。在不同的上下文中,“复杂对象图”有不同的含义。
     我在本文中的论点是,一旦对象图开始变得“复杂”,pure DI很快就会成为比使用DI容器更好的替代方案。
     pure DI可能是更好的替代方案还有其他原因,但在本文中我只讨论图的复杂性。
     请注意,这里我们可以粗略地衡量复杂性,这取决于有多少对象违反了前面两条规则。有些图表比其他图表更复杂。

以下是我们会遇到复杂对象图的一些原因:

原因(A):

  1. 我们有时使用装饰(Decorator)模式为某些对象添加功能。例如,我们可以为IEmailSender类创建一个修饰器,以便在发送电子邮件之前对其进行加密。
  2. 我们可以抽象接口背后的一些概念,并拥有两个将在应用程序中同时使用的实现。例如,考虑处理文档的应用程序的IDocumentSource接口。我们可能希望应用程序从FTP站点提取文档,同时也希望它从数据库提取文档。我们可能有一个FTPDocumentSource类和一个DatabaseDocumentSource类。然后我们可能会创建一个facade来从这两个来源获取文件。

原因(B):

 使用相同的IDocumentSource示例,我们可能有不同的FTP服务器要从中提取文档。因此,我们创建了FTPDocumentSource类的两个或多个实例。在构造函数中使用ftp服务器地址参数,我们将能够将一个对象指向ftp服务器1,将另一个对象指向ftp服务器2。
 让事情变得复杂的是,假设有一些配置文件或数据库,其中包含我们要从中提取文档的ftp服务器列表(这样的配置文件/数据库可以由应用程序管理员更新)。我们需要在应用程序启动时在运行时创建FTPDocumentSource对象。此外,基于这样的配置,我们可能决定装饰一些对象,但不是所有对象。例如,假设数据库中有以下配置表(可以在配置文件中执行相同的操作):

FTP ServerDocument types to include
ftp1.server1.labALL
ftp2.server2.labpdf;docx
ftp3.server3.labpdf

 因此,我们有FTPDocumentSource类,也有根据文档类型过滤文档的装饰。这样的装饰器可能被命名为DocumentSourceFilteringDecorator。这样的装饰器在其构造函数中接受IDocumentSource和字符串过滤器,当调用它来提取文档时,它将根据过滤器字符串过滤文档。
 在数据库中的示例配置中,第一个FTP源不需要应用修饰符。而另外两个来源确实需要这样的装饰师。
 我们还需要另一个IDocumentSource实现来使多个文档源看起来像文档源消费者的单个文档源。这样的实现可以称为AggregatedDocumentSource。

下面是对象图的这一部分的外观:
在这里插入图片描述
下面是涉及的类型的UML类图:
在这里插入图片描述
下面是如何使用Unity DI容器配置和创建此类对象图:

//Assume this comes from configuration database or file
SourceSettings[] settings =
{
    new SourceSettings { FTPServerAddress = "ftp1.server1.lab" , Filter= "ALL"},
    new SourceSettings { FTPServerAddress = "ftp2.server2.lab" , Filter= "pdf;docx"},
    new SourceSettings { FTPServerAddress = "ftp3.server3.lab" , Filter= "pdf"}
};

UnityContainer container = new UnityContainer();

List<string> names = new List<string>();

for(int i = 0 ; i < settings.Length ; i++)
{

    string ftp_document_source_name = "ftp_document_source" + i;
    string filtering_document_source_name = "filtering_document_source" + i;

    container.RegisterType<IDocumentSource, FTPDocumentSource>(
        ftp_document_source_name,
        new InjectionConstructor(settings[i].FTPServerAddress));

    if (settings[i].Filter != "ALL")
    {
        container.RegisterType<IDocumentSource, DocumentSourceFilteringDecorator>(
            filtering_document_source_name,
            new InjectionConstructor(
                new ResolvedParameter<IDocumentSource>(ftp_document_source_name),
                settings[i].Filter));

        names.Add(filtering_document_source_name);
    }
    else
    {
        names.Add(ftp_document_source_name);
    }
}

object[] document_sources_resolved_parameters =
    names.Select(x => new ResolvedParameter<IDocumentSource>(x)).Cast<object>().ToArray();


container.RegisterType<IDocumentSource, AggregatedDocumentSource>(
    new InjectionConstructor(
        new ResolvedArrayParameter<IDocumentSource>(document_sources_resolved_parameters)));

var document_source = container.Resolve<IDocumentSource>();

 从该示例中可以看出,使用DI containers创建对象图已经变得多么复杂。
 由于我们针对同一接口有多个注册,因此我们使用名称来区分不同的注册。
 在上面的代码中,我们遍历从配置数据库或文件中获取的ftp服务器列表,并针对其中的每一个,根据相应的过滤器来决定是否要使用过滤装饰器。

  1. 如果我们决定要过滤此类源,则进行2个注册,一个注册用于FTP文档源,另一个注册用于过滤装饰器。 我们使用FTP文档源注册的名称链接FTP文档源及其相应的过滤修饰器。 我们将过滤装饰器注册的名称存储在名称列表中,因为在创建AggregatedDocumentSource时将需要它。
  2. 如果我们决定不过滤文档源,则只需对FTP文档源进行一次注册,然后为其命名。 同样,我们存储了注册名称。

注意我们如何使用索引“ i”创建唯一的名称。
 最后,我们需要注册AggregatedDocumentSource。 此类在其构造函数中接受IDocuemntSource对象的数组。 我们使用在循环中收集的名称将此注册指向需要链接到的文档源。
 如您所见,我们不再使用自动装配功能。 就像我们是在手动构建图表一样,但是具有更多的冗长性。
现在,让我们看一下如何使用pure DI来构建这样的对象图。

SourceSettings[] settings =
{
    new SourceSettings { FTPServerAddress = "ftp1.server1.lab" , Filter= "ALL"},
    new SourceSettings { FTPServerAddress = "ftp2.server2.lab" , Filter= "pdf;docx"},
    new SourceSettings { FTPServerAddress = "ftp3.server3.lab" , Filter= "pdf"}
};


List<IDocumentSource> sources = new List<IDocumentSource>();

foreach (var source_settings in settings)
{
    IDocumentSource source = new FTPDocumentSource(source_settings.FTPServerAddress);

    if (source_settings.Filter != "ALL")
        source = new DocumentSourceFilteringDecorator(source, source_settings.Filter);

    sources.Add(source);
}

var document_source = new AggregatedDocumentSource(sources.ToArray());

在这里,很明显,在这种情况下使用pure DI会生成更具可读性和可维护性的代码。
 当我们开始拥有复杂的图形时,我们将失去使用DI containers提供的自动装配功能自动装配图形的能力。 大多数注册将具有名称(或具有类似后果的其他DI containers功能),并且我们将自己手动连接对象,但与pure DI相比,它们具有更多的代码和更少的编译时验证支持。
 对于违反简单图形属性的类型,不需要命名注册。 一旦开始拥有这样的命名注册,某些依赖于用名称注册的类型的对象也将需要名称进行注册,请看以下示例:
在这里插入图片描述
 在这里,我们有一个DocumentsProcessor类,该类在其构造函数中接收IDocumentSource,并在调用其Process方法时将处理源中的文档。
 假设我们要为每个文档源创建一个DocumentProcessor,即我们要有以下图形(所有3个DocumentProcessor对象将在一个图形中使用):
在这里插入图片描述
 在这种情况下,即使您没有IDocumentProcessor的多个实现,您仍需要命名3个DocumentProcessor注册中的每一个,因为它们每个都依赖于IDocumentSource的不同配置。

概括:

 对于简单的对象图,DI containers 可能是一个不错的选择。 尽管以我的经验来看,图在项目生命周期中往往会变得非常复杂。 如果使用DI containers,一旦图形变得复杂,您将不得不切换到pure DI或使用大量命名注册。
 对于复杂图形,pure DI更好。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值