代码之家  ›  专栏  ›  技术社区  ›  Konstantin Konstantinov

带有太多构造函数参数的Unity注入

  •  3
  • Konstantin Konstantinov  · 技术社区  · 7 年前

    我有以下与统一有关的问题。下面的代码存根设置了基本场景,问题在底部。

    注意, [Dependency] 对于下面的示例,属性不起作用,导致 StackoverflowException 但构造函数注入确实有效。

    注(2)下面的一些注释开始分配“标签”,如代码气味、糟糕的设计等…因此,为了避免混淆,这里是没有任何设计的业务设置。

    这个问题似乎引起了一场严重的争论,甚至在一些最著名的首席专家中也是如此。事实上,这个问题远远超出了C的范畴,它更多地属于纯计算机科学。这个问题是基于服务定位器模式和纯依赖注入模式之间的众所周知的“战斗”而提出的: https://martinfowler.com/articles/injection.html VS http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/ 以及随后的更新,以弥补依赖注入变得过于复杂的情况: http://blog.ploeh.dk/2010/02/02/RefactoringtoAggregateServices/

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

    我有一个很大的(50多个)集合,我称之为微服务。如果你有更好的名字,请在阅读时“应用”它。它们中的每一个操作一个对象,我们称之为引号。然而,元组(上下文+引号)似乎更合适。Quote是一个业务对象,它被处理并序列化到数据库中,上下文是一些支持信息,这在处理Quote时是必需的,但不会保存到数据库中。其中一些支持信息实际上可能来自数据库或某些第三方服务。这无关紧要。装配线是一个现实世界的例子:装配工人(微服务)接收一些输入(指令(上下文)+部件(引用)),处理它(根据指令对部件进行处理和/或修改指令),如果成功则进一步传递它,如果出现问题则丢弃它(引发异常)。微服务最终被打包成一小部分(大约5个)高级服务。这种方法将一些非常复杂的业务对象的处理线性化,并允许分别测试每个微服务和其他微服务:只需给它一个输入状态,并测试它产生的预期输出。

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

    这与Mark上面的建议严重冲突:如果我在一些高级服务中对一个报价应用了10+个有效独立的规则,那么,根据第三个博客,我应该将它们聚合到一些逻辑组中,比如说不超过3-4个,而不是通过构造函数注入所有10+。但是没有逻辑组!虽然有些规则是松散依赖的,但大多数规则不是,因此,人为地将它们捆绑在一起会带来更多的伤害而非好处。

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

    我甚至没有提到规则依赖于美国的状态,因此,理论上,大约有50个规则集合,每个状态和每个工作流都有一个集合。虽然有些规则是在所有州之间共享的(比如“保存对数据库的引用”),但是有很多州特定的规则。

    下面是一个非常简单的例子。

    Quote—业务对象,保存到数据库中。

    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;
        }
    

    然后统一注册所有类型:

    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 ,很容易创建一个自动单元测试,它将迭代所有属性并验证所有服务都可以实例化,这意味着不会出现令人讨厌的运行时意外。

    这种方法的问题在于它开始看起来很像服务定位器,有些人认为它是反模式的: http://blog.ploeh.dk/2010/02/03/servicelocatorisananti-pattern/ 然后人们开始争辩说,所有的依赖关系都必须明确化,而不是像在 ServiceRepository .

    我该怎么办?

    3 回复  |  直到 7 年前
        1
  •  5
  •   Christian Gollhardt    7 年前

    我只创建一个接口:

    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 但是它并没有隐藏依赖关系,所以我认为这是可以的。

        2
  •  2
  •   Mark Seemann    7 年前

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

    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);
    }
    

    这是一个很好的抽象,因为它是 endomorphism 结束 Quote ,我们知道它是可组合的。 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中。下面是一个简单的例子:

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

    这样的组合应该放在应用程序的 Composition Root .

    各种服务可以有自己的依赖关系:

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

    如果有用的话,甚至可以嵌套复合材料:

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

    这很好地解决了 施工人员超注 代码气味,因为 CompositeQuoteProcessor 类只有一个依赖项。但是,由于该单个依赖项是一个集合,因此可以任意组合许多其他处理器。

    在这个答案中,我完全忽视了统一。依赖注入是一个软件设计问题。如果一个DI容器不能很容易地组成一个好的设计,你最好 Pure DI 我在这里已经暗示过了。


    如果你 必须 使用Unity,可以始终创建派生自 复合报价处理器 并采取 Concrete Dependencies :

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

    Unity应该能够自动连接那个类,然后…

        3
  •  -1
  •   Scott Chamberlain    7 年前

    Unity支持属性注入。而不是将所有这些值传递给构造函数,而是使用 [Dependency] 属性。这允许您根据需要添加尽可能多的注入,而不必更新构造函数。

    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);
    }