如何避免依赖注入构造函数的疯狂?

Posted

技术标签:

【中文标题】如何避免依赖注入构造函数的疯狂?【英文标题】:How to avoid Dependency Injection constructor madness? 【发布时间】:2011-01-26 02:03:12 【问题描述】:

我发现我的构造函数开始看起来像这样:

public MyClass(Container con, SomeClass1 obj1, SomeClass2, obj2.... )

随着参数列表的不断增加。既然“Container”是我的依赖注入容器,我为什么不能这样做:

public MyClass(Container con)

每个班级?有什么缺点?如果我这样做,感觉就像我在使用美化的静态。请分享您对 IoC 和依赖注入疯狂的看法。

【问题讨论】:

你为什么要通过容器?我想你可能误解了国际奥委会 如果你的构造函数需要更多或更多的参数,你可能在这些类中做的太多了。 这不是你进行构造函数注入的方式。对象根本不知道 IoC 容器,也不应该知道。 您可以只创建一个空的构造函数,在其中直接调用 DI 请求您需要的东西。这将消除构造函数的疯狂,但您需要确保您使用的是 DI 接口..以防您在开发过程中更改您的 DI 系统。老实说.. 没有人会支持这样做,即使这是 DI 注入构造函数的方法。呵呵 【参考方案1】:

你是对的,如果你将容器用作服务定位器,它或多或少是一个美化的静态工厂。出于多种原因I consider this an anti-pattern(另见我书中的this excerpt)。

构造函数注入的一大好处是它使违反Single Responsibility Principle 的行为非常明显。

当这种情况发生时,是时候refactor to Facade Services了。简而言之,创建一个新的、更粗粒度界面,隐藏您当前需要的部分或全部细粒度依赖项之间的交互。

【讨论】:

+1 用于将重构工作量化为一个概念;太棒了:) 真的吗?您刚刚创建了一个间接将这些参数移动到另一个类,但它们仍然存在!只是处理起来更复杂。 @irreputable:在退化的情况下,我们将 all 依赖项移动到聚合服务中,我同意这只是另一个没有好处的间接级别,所以我选择的词稍微偏离了。然而,关键是我们只将 一些 细粒度的依赖项移动到聚合服务中。这限制了新聚合服务和留下的依赖项中的依赖项排列的数量。这使得两者都更容易处理。 最好的评论:“构造函数注入的一个美妙的好处是它使违反单一责任原则的行为非常明显。” @DonBox 在这种情况下,您可以编写空对象实现来停止递归。不是你需要的,但关键是构造函数注入并不能防止循环 - 它只是清楚地表明它们在那里。【参考方案2】:

我认为您的类构造函数不应该引用您的 IOC 容器周期。这表示您的类和容器之间存在不必要的依赖关系(IOC 试图避免的依赖类型!)。

【讨论】:

+1 依赖于 IoC 容器使得以后在不更改所有其他类中的一堆代码的情况下很难更改该容器 如何在没有构造函数接口参数的情况下实现 IOC?我是不是看错你的帖子了? @J Hunt 我不明白你的评论。对我来说,接口参数意味着作为依赖项接口的参数,即,如果您的依赖项注入容器初始化MyClass myClass = new MyClass(IDependency1 interface1, IDependency2 interface2)(接口参数)。这与@derivation 的帖子无关,我将其解释为依赖注入容器不应将自身注入其对象,即MyClass myClass = new MyClass(this) 我不明白在某些情况下如何避免传递 IoC 容器。示例(可能是唯一有效的示例):工厂。 cs class MyTypeFactory private readonly IServiceProvier mServices; public MyTypeFactory(IServiceProvier services) => mServices = services; MyType Create(int param) => ActivatorUtilities.CreateInstance<MyType>(mServices, param); 【参考方案3】:

传入参数的难度不是问题。问题是你的班级做得太多了,应该更多地分解。

依赖注入可以作为类变得太大的早期警告,特别是因为传递所有依赖项的痛苦越来越大。

【讨论】:

如果我错了,请纠正我,但在某些时候你必须“将所有东西粘合在一起”,因此你必须为此获得多个依赖项。例如在视图层中,当为它们构建模板和数据时,您必须从各种依赖项(例如“服务”)中获取所有数据,然后将所有这些数据放入模板和屏幕中。如果我的网页有 10 个不同的信息“块”,那么我需要 10 个不同的类来为我提供这些数据。所以我的视图/模板类需要 10 个依赖项?【参考方案4】:

您使用的是什么依赖注入框架?您是否尝试过使用基于 setter 的注入?

基于构造函数的注入的好处是对于不使用 DI 框架的 Java 程序员来说它看起来很自然。您需要 5 件事来初始化一个类,然后您的构造函数有 5 个参数。缺点是您已经注意到,当您有很多依赖项时,它会变得笨拙。

使用 Spring,您可以使用 setter 传递所需的值,并且可以使用 @required 注释来强制它们被注入。缺点是您需要将初始化代码从构造函数移动到另一个方法,并在通过使用@PostConstruct 标记注入所有依赖项之后让 Spring 调用该方法。我不确定其他框架,但我认为它们会做类似的事情。

两种方式都可以,这是一个偏好问题。

【讨论】:

构造函数注入的原因是为了让依赖关系明显,而不是因为它对java开发者来说看起来更自然。 迟到的评论,但这个答案让我笑了:) +1 用于基于 setter 的注入。如果我在我的类中定义了服务和存储库,那么它们非常明显它们是依赖项。我不需要编写大量看起来像 VB6 的构造函数,也不需要在构造函数中做愚蠢的分配代码。很明显,所需字段的依赖关系是什么。 根据 2018 年,Spring 官方建议不要使用 setter 注入,除非依赖项具有合理的默认值。如,如果依赖项对类是强制性的,则建议使用构造函数注入。见discussion on setter vs ctor DI【参考方案5】:

我遇到了一个类似的问题,关于基于构造函数的依赖注入以及传递所有依赖项的复杂程度。

我过去使用的一种方法是使用服务层的应用程序外观模式。这将有一个粗略的 API。如果此服务依赖于存储库,它将使用私有属性的 setter 注入。这需要创建一个抽象工厂,并将创建存储库的逻辑转移到一个工厂中。

详细代码和解释可以在这里找到

Best practices for IoC in complex service layer

【讨论】:

【参考方案6】:

问题:

1) 具有不断增加的参数列表的构造函数。

2) 如果类是继承的(例如:RepositoryBase)然后更改构造函数 签名会导致派生类发生变化。

解决方案 1

IoC Container 传递给构造函数

为什么

不再增加参数列表 构造函数的签名变得简单

为什么不

使您的类与 IoC 容器紧密耦合。 (当 1. 你想在使用不同 IoC 容器的其他项目中使用该类时会导致问题。2. 你决定更改 IoC 容器) 让你的类不那么具有描述性。 (你不能真正查看类构造函数并说出它需要什么才能运行。) 类可以访问潜在的所有服务。

解决方案 2

创建一个将所有服务分组的类并将其传递给构造函数

 public abstract class EFRepositoryBase 
 
    public class Dependency
    
        public DbContext DbContext  get; 
        public IAuditFactory AuditFactory  get; 

         public Dependency(
            DbContext dbContext,
            IAuditFactory auditFactory)
        
            DbContext = dbContext;
            AuditFactory = auditFactory;
        
    

    protected readonly DbContext DbContext;        
    protected readonly IJobariaAuditFactory auditFactory;

    protected EFRepositoryBase(Dependency dependency)
    
        DbContext = dependency.DbContext;
        auditFactory= dependency.JobariaAuditFactory;
    
  

派生类

  public class ApplicationEfRepository : EFRepositoryBase      
  
     public new class Dependency : EFRepositoryBase.Dependency
     
         public IConcreteDependency ConcreteDependency  get; 

         public Dependency(
            DbContext dbContext,
            IAuditFactory auditFactory,
            IConcreteDependency concreteDependency)
        
            DbContext = dbContext;
            AuditFactory = auditFactory;
            ConcreteDependency = concreteDependency;
        
     

      IConcreteDependency _concreteDependency;

      public ApplicationEfRepository(
          Dependency dependency)
          : base(dependency)
       
        _concreteDependency = dependency.ConcreteDependency;
      
   

为什么

向类添加新依赖不会影响派生类 类与 IoC 容器无关 类是描述性的(就其依赖项而言)。按照惯例,如果你想知道 A 依赖于什么类,那信息就累积在 A.Dependency 构造函数签名变得简单

为什么不

需要创建额外的类 服务注册变得复杂(需要每个X.Dependency单独注册) 概念上与传递IoC Container相同 ..

解决方案 2 只是一个原始的解决方案,如果有充分的理由反对它,那么描述性评论将不胜感激

【讨论】:

解析器会在构造函数中依赖注入数据上下文和接口。【参考方案7】:

这是我使用的方法

public class Hero


    [Inject]
    private IInventory Inventory  get; set; 

    [Inject]
    private IArmour Armour  get; set; 

    [Inject]
    protected IWeapon Weapon  get; set; 

    [Inject]
    private IAction Jump  get; set; 

    [Inject]
    private IInstanceProvider InstanceProvider  get; set; 



这是一个粗略的方法,如何在注入值后执行注入和运行构造函数。这是功能齐全的程序。

public class InjectAttribute : Attribute





public class TestClass

    [Inject]
    private SomeDependency sd  get; set; 

    public TestClass()
    
        Console.WriteLine("ctor");
        Console.WriteLine(sd);
    


public class SomeDependency





class Program

    static void Main(string[] args)
    
        object tc = FormatterServices.GetUninitializedObject(typeof(TestClass));

        // Get all properties with inject tag
        List<PropertyInfo> pi = typeof(TestClass)
            .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public)
            .Where(info => info.GetCustomAttributes(typeof(InjectAttribute), false).Length > 0).ToList();

        // We now happen to know there's only one dependency so we take a shortcut just for the sake of this example and just set value to it without inspecting it
        pi[0].SetValue(tc, new SomeDependency(), null);


        // Find the right constructor and Invoke it. 
        ConstructorInfo ci = typeof(TestClass).GetConstructors()[0];
        ci.Invoke(tc, null);

    

我目前正在从事一个像这样工作的爱好项目 https://github.com/Jokine/ToolProject/tree/Core

【讨论】:

【参考方案8】:

我把这整个帖子读了两遍,我认为人们是根据他们所知道的而不是被问到的。

JP 的原始问题看起来像是通过发送解析器来构造对象,然后是一堆类,但我们假设这些类/对象本身就是服务,可以注入。如果不是呢?

JP,如果您希望利用 DI 并希望获得将注入与上下文数据混合的荣耀,那么这些模式(或所谓的“反模式”)都没有专门解决这个问题。它实际上归结为使用可以支持您进行此类努力的软件包。

Container.GetSevice<MyClass>(someObject1, someObject2)

...这种格式很少被支持。我相信编写这种支持的难度,加上与实现相关的糟糕性能,使得它对开源开发人员没有吸引力。

但应该这样做,因为我应该能够为 MyClass'es 创建和注册一个工厂,并且该工厂应该能够接收数据/输入,而不是仅仅为为了传递数据。如果“反模式”是关于负面后果,那么强制存在用于传递数据/模型的人工服务类型肯定是负面的(与您将类包装到容器中的感觉相当。同样的直觉适用)。

有一些框架可能会有所帮助,即使它们看起来有点难看。比如忍者:

Creating an instance using Ninject with additional parameters in the constructor

这适用于 .NET,很受欢迎,但仍然没有应有的干净,但我敢肯定,无论您选择使用哪种语言,都会有一些东西。

【讨论】:

【参考方案9】:

注入容器是你最终会后悔的捷径。

过度注入不是问题,它通常是其他结构缺陷的症状,最明显的是关注点分离。 这不是一个问题,但可能有很多来源,而使这个问题难以解决的原因是您将不得不处理所有这些问题,有时同时处理(想想解开意大利面条)。

以下是需要注意的事项的不完整列表

糟糕的域设计(聚合根的......等)

关注点分离不佳(服务组合、命令、查询)请参阅 CQRS 和事件溯源。

OR 映射器(小心,这些东西会给你带来麻烦)

查看模型和其他 DTO(切勿重复使用,并尽量减少它们!!!!)

【讨论】:

以上是关于如何避免依赖注入构造函数的疯狂?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Xamarin 和 Autofac 将构造函数依赖项注入 ViewModel?

如何使用构造函数依赖注入对 asp.net 核心应用程序进行单元测试

如何在其构造函数中获取使用依赖注入的类的实例

如何使用.NET Core依赖注入来解析一个对象需要一个对象的构造函数?

Spring-基于构造函数的依赖注入

Ninject之旅之七:Ninject依赖注入