具有过多构造函数参数的Unity注入

时间:2018-08-21 00:47:09

标签: c# dependency-injection unity-container

我有以下与Unity有关的问题。下面的代码存根设置了基本方案,问题在底部。

注意,该[Dependency]属性在以下示例中不起作用,并产生StackoverflowException,但构造函数注入确实起作用。

注意(2)下面的一些注释开始分配“标签”,例如代码异味,不良设计等。因此,为避免混淆,此处是首先没有任何设计的业务设置。

即使在某些最著名的C#专家中,这个问题似乎也引起了严重的争议。实际上,这个问题远远超出了C#,它更多地属于纯计算机科学。这个问题基于服务定位器模式和纯依赖项注入模式之间的众所周知的“战斗”:https://martinfowler.com/articles/injection.htmlhttp://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/,以及随后的更新以解决依赖项注入变得过于复杂时的情况: http://blog.ploeh.dk/2010/02/02/RefactoringtoAggregateServices/

这里是这种情况,它与前两个描述的内容不太吻合,但似乎与第一个描述的内容完全吻合。

我收集了一大堆(超过50种)所谓的微服务。如果您有更好的名字,请在阅读时“应用”它。它们中的每一个都对单个对象进行操作,我们称其为引用。但是,元组(上下文+引号)似乎更合适。报价是一个业务对象,它被处理并序列化到数据库中,而上下文是一些支持信息,这在处理报价时是必需的,但是没有保存到数据库中。其中一些支持信息实际上可能来自数据库或某些第三方服务。这无关紧要。组装线是一个真实的例子:组装工人(微服务)接收一些输入(指令(上下文)+零件(引用)),对其进行处理(根据指令对零件进行某些处理和/或修改指令)并在成功的情况下将其进一步传递,或在出现问题时将其丢弃(引发异常)。最终,这些微服务被捆绑为少量(约5个)高级服务。这种方法使某些非常复杂的业务对象的处理线性化,并允许将每个微服务与所有其他微服务分开进行测试:只需为其提供输入状态并测试其是否会产生预期的输出即可。

在这里变得很有趣。由于涉及的步骤很多,高级服务开始依赖于许多微服务:10多个及更多。这种依赖性是很自然的,它仅反映了基础业务对象的复杂性。最重要的是,几乎可以恒定地添加/删除微服务:基本上,它们是一些业务规则,几乎像水一样流动。

这与Mark的建议严重冲突:如果我在某项高级服务中将10多个有效的独立规则应用于报价,那么根据第三个博客,我应该将它们聚合为一些逻辑组,比如说不超过3-4,而不是通过构造函数注入所有10+。但是没有逻辑组!尽管其中一些规则是松散依赖的,但大多数规则却并非如此,因此,人为地捆绑在一起会弊大于利。

导致规则频繁更改,这成为维护的噩梦:每次更改规则时,所有真实/模拟的调用都必须更新。

而且我什至没有提到这些规则取决于美国的州,因此,从理论上讲,大约有50个规则集合,每个州和每个工作流程都有一个集合。而且,虽然某些规则在所有状态之间共享(例如“将报价保存到数据库中”),但是有许多特定于状态的规则。

这是一个非常简化的示例。

报价-业务对象,该对象将保存到数据库中。

public class Quote
{
    public string SomeQuoteData { get; set; }
    // ...
}

微服务。他们每个人都执行一些小的更新来报价。也可以从一些较低级别的微服务中构建较高级别的服务。

public interface IService_1
{
    Quote DoSomething_1(Quote quote);
}
// ...

public interface IService_N
{
    Quote DoSomething_N(Quote quote);
}

所有微服务都从该接口继承。

public interface IQuoteProcessor
{
    List<Func<Quote, Quote>> QuotePipeline { get; }
    Quote ProcessQuote(Quote quote = null);
}

// Low level quote processor. It does all workflow related work.
public abstract class QuoteProcessor : IQuoteProcessor
{
    public abstract List<Func<Quote, Quote>> QuotePipeline { get; }

    public Quote ProcessQuote(Quote quote = null)
    {
        // Perform Aggregate over QuotePipeline.
        // That applies each step from workflow to a quote.
        return quote;
    }
}

一种高级“工作流”服务。

public interface IQuoteCreateService
{
    Quote CreateQuote(Quote quote = null);
}

及其使用许多底层微服务的实际实现。

public class QuoteCreateService : QuoteProcessor, IQuoteCreateService
{
    protected IService_1 Service_1;
    // ...
    protected IService_N Service_N;

    public override List<Func<Quote, Quote>> QuotePipeline =>
        new List<Func<Quote, Quote>>
        {
            Service_1.DoSomething_1,
            // ...
            Service_N.DoSomething_N
        };

    public Quote CreateQuote(Quote quote = null) => 
        ProcessQuote(quote);
}

实现DI的主要方法有两种:

标准方法是通过构造函数注入所有依赖项:

    public QuoteCreateService(
        IService_1 service_1,
        // ...
        IService_N service_N
        )
    {
        Service_1 = service_1;
        // ...
        Service_N = service_N;
    }

然后在Unity中注册所有类型:

public static class UnityHelper
{
    public static void RegisterTypes(this IUnityContainer container)
    {
        container.RegisterType<IService_1, Service_1>(
            new ContainerControlledLifetimeManager());
        // ...
        container.RegisterType<IService_N, Service_N>(
            new ContainerControlledLifetimeManager());

        container.RegisterType<IQuoteCreateService, QuoteCreateService>(
            new ContainerControlledLifetimeManager());
    }
}

然后Unity将执行其“魔术”并在运行时解析所有服务。问题是,目前我们大约有30个这样的微服务,并且数量预计会增加。随后,一些构造函数已经注入了10多个服务。这不方便维护,模拟等...

当然,可以从这里使用这个想法:http://blog.ploeh.dk/2010/02/02/RefactoringtoAggregateServices/但是,微服务之间并没有真正的关联,因此将它们捆绑在一起是一个没有任何道理的人为过程。此外,它还会破坏使整个工作流程线性且独立的目的(微服务采用当前的“状态”,然后执行带引号的某些操作,然后继续执行)。他们都不关心之前或之后的任何其他微服务。

另一个想法似乎是创建一个“服务存储库”:

public interface IServiceRepository
{
    IService_1 Service_1 { get; set; }
    // ...
    IService_N Service_N { get; set; }

    IQuoteCreateService QuoteCreateService { get; set; }
}

public class ServiceRepository : IServiceRepository
{
    protected IUnityContainer Container { get; }

    public ServiceRepository(IUnityContainer container)
    {
        Container = container;
    }

    private IService_1 _service_1;

    public IService_1 Service_1
    {
        get => _service_1 ?? (_service_1 = Container.Resolve<IService_1>());
        set => _service_1 = value;
    }
    // ...
}

然后在Unity中注册它,并将所有相关服务的构造函数更改为如下形式:

    public QuoteCreateService(IServiceRepository repo)
    {
        Service_1 = repo.Service_1;
        // ...
        Service_N = repo.Service_N;
    }

这种方法的优点(与前一种方法相比)如下:

所有微服务和更高级别的服务都可以以统一的形式创建:可以轻松添加/删除新的微服务,而无需修复对服务和所有单元测试的构造函数调用。随后,维护和复杂性降低。

由于使用了IServiceRepository界面,因此很容易创建一个自动化的单元测试,该单元测试将遍历所有属性并验证可以实例化所有服务,这意味着运行时不会感到意外。 / p>

这种方法的问题在于,它开始看起来很像服务定位器,有人认为这是一种反模式:http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/,然后人们开始争辩说,必须将所有依赖项都明确化并不像在ServiceRepository中那样被隐藏。

我该怎么办?

4 个答案:

答案 0 :(得分:5)

我只创建一个界面:

public interface IDoSomethingAble
{
    Quote DoSomething(Quote quote);
}

还有一个汇总:

public interface IDoSomethingAggregate : IDoSomethingAble {}

public class DoSomethingAggregate : IDoSomethingAggregate 
{
    private IEnumerable<IDoSomethingAble> somethingAbles;

    public class DoSomethingAggregate(IEnumerable<IDoSomethingAble> somethingAbles)
    {
        _somethingAbles = somethingAbles;
    }

    public Quote DoSomething(Quote quote)
    {
        foreach(var somethingAble in _somethingAbles)
        {
            somethingAble.DoSomething(quote);
        }
        return quote;
    }
}

注意:依赖注入并不意味着,您需要在任何地方使用它。

我要去工厂

public class DoSomethingAggregateFactory
{
    public IDoSomethingAggregate Create()
    {
        return new DoSomethingAggregate(GetItems());
    }

    private IEnumerable<IDoSomethingAble> GetItems()
    {
        yield return new Service1();
        yield return new Service2();
        yield return new Service3();
        yield return new Service4();
        yield return new Service5();
    }
}

其他所有内容(未注入构造函数)都隐藏了显式依赖项。


作为最后的选择,您还可以创建一些DTO对象,并通过构造函数注入每个需要的服务(但只有一次)。

这样,您可以请求ProcessorServiceScope并提供所有服务,而无需为每个类创建ctor逻辑:

public class ProcessorServiceScope
{
    public Service1 Service1 {get;};
    public ServiceN ServiceN {get;};

    public ProcessorServiceScope(Service1 service1, ServiceN serviceN)
    {
        Service1 = service1;
        ServiceN = serviceN;
    }
}

public class Processor1
{
    public Processor1(ProcessorServiceScope serviceScope)
    {
        //...
    }
}

public class ProcessorN
{
    public ProcessorN(ProcessorServiceScope serviceScope)
    {
        //...
    }
}

它看起来像ServiceLocator,但是它并没有掩盖这些缺陷,所以我认为这还可以。

答案 1 :(得分:2)

考虑列出的各种接口方法:

Quote DoSomething_1(Quote quote);
Quote DoSomething_N(Quote quote);
Quote ProcessQuote(Quote quote = null)
Quote CreateQuote(Quote quote = null);

除名称外,它们都是相同的。为什么使事情变得如此复杂?考虑到Reused Abstractions Principle,我认为如果您有更少的抽象和更多的实现会更好。

因此,引入一个抽象:

public interface IQuoteProcessor
{
    Quote ProcessQuote(Quote quote);
}

这是一个很好的抽象,因为它是Quote之上的endomorphism,我们知道它是可组合的。 You can always create a Composite of an endomorphism

public class CompositeQuoteProcessor : IQuoteProcessor
{
    private readonly IReadOnlyCollection<IQuoteProcessor> processors;

    public CompositeQuoteProcessor(params IQuoteProcessor[] processors)
    {
        this.processors = processors ?? throw new ArgumentNullException(nameof(processors));
    }

    public Quote ProcessQuote(Quote quote)
    {
        var q = quote;
        foreach (var p in processors)
            q = p.ProcessQuote(q);
        return q;
    }
}

至此,我认为您已经完成了。您现在可以组成各种服务(在OP中称为 microservices )。这是一个简单的示例:

var processor = new CompositeQuoteProcessor(new Service1(), new Service2());

这种组成应该放在应用程序的Composition Root中。

各种服务可以具有自己的依赖性:

var processor =
    new CompositeQuoteProcessor(
        new Service3(
            new Foo()),
        new Service4());

如果有用,您甚至可以嵌套Composites:

var processor =
    new CompositeQuoteProcessor(
        new CompositeQuoteProcessor(
            new Service1(),
            new Service2()),
        new CompositeQuoteProcessor(
            new Service3(
                new Foo()),
            new Service4()));

这很好地解决了构造函数过度注入的代码异味,因为CompositeQuoteProcessor类仅具有单个依赖项。但是,由于单个依赖项是一个集合,因此您可以任意组合许多其他处理器。

在这个答案中,我完全忽略了Unity。依赖注入是软件设计的问题。如果DI容器不能轻松地组成一个好的设计,那么我建议在这里使用Pure DI会更好。


如果必须使用Unity,则始终可以创建从CompositeQuoteProcessor派生并采用Concrete Dependencies的具体类:

public class SomeQuoteProcessor1 : CompositeQuoteProcessor
{
    public SomeQuoteProcessor1(Service1 service1, Service3 service3) :
        base(service1, service3)
    {
    }
}

Unity应该能够自动连接该类,然后...

答案 2 :(得分:0)

我从来没有想过要回答我自己的问题,尽管功劳很大一部分应该归功于https://softwareengineering.stackexchange.com/users/115084/john-wu-他是我有正确方向的人。

尽管如此,自从我问这个问题以来已经过去了将近两年的时间,虽然我在提出问题后稍稍建立了问题的解决方案(并感谢所有回答的人),但大多数情况下还是花了一年多的时间。我所工作的公司中的开发人员实际上了解了它是如何工作的以及它是做什么的(是的,他们都远高于普通开发人员,是的,代码是用纯C#编写的,没有外部库)。因此,我认为这对于其他可能具有类似业务场景的人来说很重要。

如问题中所述,我们问题的根源在于我们要处理的参数空间太大。我们称为工作流的6-8个值(称为W),称为状态配置的30-40个值(称为S)–这是美国状态代码和其他两个参数的组合并非所有三元组都是可能的(状态配置的实际内容是无关紧要的),大约30-50个我们称为风险规则的值(称为R)-该值取决于乘积,但这与无关紧要不同的产品有不同的对待。

因此,参数空间的总尺寸为N = W * S * R,大约为10K(我不太关心精确值)。这意味着在代码运行时,我们大约需要以下内容:对于每个工作流程(显然一次仅运行一个,但它们全部都在某个时间运行)和每个状态配置(同样一次仅运行一个,但是它们中的任何一个都可能在某个时间运行),我们需要评估所有与该工作流程和状态配置相关的风险规则。

好吧,如果参数空间的维数大约为N,则覆盖整个空间所需的测试数量至少应为N的数量级。这正是遗留代码和测试试图做到的以及导致问题的原因。 答案是在纯数学中,而不是在纯计算机科学中,并且它基于所谓的“可分离空间:https://en.wikipedia.org/wiki/Separable_space”和在组论术语中称为“不可约表示”:{{3} }。尽管我不得不承认,后者更像是一种灵感,而不是群体理论的实际应用。

如果您已经迷失了我,那很好。只是,请先阅读上述数学运算,然后再继续。

这里的空间可分离性意味着我们可以选择这样的空间N,以使子空间W,S和R变得独立(或可分离)。据我所知,对于在CS中处理的有限空间,总可以做到这一点。

这意味着我们可以将N空间描述为S列出(或设置)一些规则,而通过为每个规则分配一组适用的工作流程,每个规则适用于W的某些工作流程。是的,如果我们有一些错误的规则本来应该应用于工作流和状态配置的某些奇怪组合中,那么我们可以将它们分解为多个规则,这样就可以保持可分离性。

这当然可以概括,但是由于它们无关紧要,因此我将跳过这些细节。

在这一点上,有人可能会想知道这是什么意思。好吧,如果我们可以将N维空间(在本例中N大约为10K)划分为独立的子空间,那么魔术就发生了,而不是按照N = W * S * R测试的顺序来写以覆盖整个参数空间,只需要编写W + S + R测试的顺序即可覆盖整个参数空间。 在我们的例子中,差异约为100倍

但这还不是全部。正如我们可以在集或列表的概念(取决于需求)中描述子空间一样,自然地将我们带入了无用测试的概念。

等等,我只是说了无用的测试吗?是的,我做到了。让我解释。典型的TDD范例是,如果代码失败,那么我们要做的第一件事就是创建一个测试,该测试将捕获该错误。好吧,如果代码是由静态列表或集合(==列表或在硬编码中设置的集合)描述的,而测试将由该列表/集合的身份转换来描述,那么这将使这种测试无用因为它必须重复原始列表/集…

状态配置形成了一个历史模式,例如,让我们在2018年的某个时候对CA州制定了一些规则。在2019年,这组规则可能会略微更改为其他一些规则,而纳入2020年的一组规则中。这些变化很小:一组规则可能会起用或丢失一些规则,并且/或者可能会对规则进行一些调整,例如如果我们正在比较某个阈值之上的某个值,则对于某些状态配置,该阈值的值可能会在某个时候更改。并且一旦更改了规则或规则集合,则它应保持原样,直到再次更改。同时,其他一些规则可能会更改,而每一个此类更改都需要引入所谓的状态配置。因此,对于每个美国州,我们都订购了这些状态配置的集合(列表),对于每个州配置,我们都有规则的集合。大多数规则都不会更改,但其中一些规则会偶尔发生更改,如所述。一种自然的IOC方法是使用IOC容器注册每个规则集合和每个状态配置的每个规则。 Unity使用状态配置的唯一“名称”和规则/集合的名称的组合(实际上,我们在工作流程中运行了多个规则集合),而每条规则已经具有一组适用的工作流程。然后,当代码针对给定的状态配置和给定的工作流程运行时,我们可以将集合从Unity中拉出。然后,一个集合包含应运行的规则的名称。然后将规则名称与状态配置名称结合起来,我们可以从Unity中拉出实际规则,过滤集合以仅保留适用于给定工作流程的规则,然后应用所有规则。 此处发生的是规则名称/集合名称形成了一些封闭的集合,通过以这种方式描述它们可以极大地受益。我们显然不希望手动为每个状态配置注册每个规则/集合,因为那样会很繁琐且容易出错。因此,我们使用所谓的“规范化器”。假设我们有一条通用规则,那就是所有状态配置都相同的规则。然后,我们仅按名称进行注册,规范化器将“自动”为所有状态配置进行注册。历史版本控制也是如此。一旦我们通过规则/集合名称+状态配置向Unity注册了一个规则/集合,那么规范化器将填补空白,直到我们在以后的某个状态配置中更改规则为止。

结果,每个规则变得极其简单。它们中的大多数具有零个或一个注入的构造函数参数,其中一些具有两个,而我只知道一个规则具有三个注入的参数。由于规则是独立且非常简单的,因此规则的测试也变得非常简单。

我们确实有一些想法可以使我在开源上面写的内容成为核心,前提是它可以为社区带来一些价值...

答案 3 :(得分:-1)

Unity支持属性注入。无需将所有这些值传递给构造函数,而只需使用[Dependency]属性提供的公共setter。这使您可以根据需要添加任意数量的注入,而不必更新构造函数。

public class QuoteCreateService : QuoteProcessor, IQuoteCreateService
{
    [Dependency]
    protected IService_1 Service_1 { get; public set; }
    // ...
    [Dependency]
    protected IService_N Service_N; { get; public set; }

    public override QuoteUpdaterList QuotePipeline =>
        new QuoteUpdaterList
        {
            Service_1.DoSomething_1,
            // ...
            Service_N.DoSomething_N
        };

    public Quote CreateQuote(Quote quote = null) => 
        ProcessQuote(quote);
}
相关问题