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


300

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

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

不断增加的参数列表。由于“容器”是我的依赖项注入容器,所以为什么我不能这样做:

public MyClass(Container con)

每堂课?不利之处是什么?如果执行此操作,则感觉就像我在使用精美的静态方法。请分享您对IoC和依赖注入疯狂的想法。


64
你为什么要通过容器?我想你可能误解IOC
保罗·克雷西

33
如果您的构造函数要求更多或更多的参数,则您可能在这些类中做的太多。
奥斯丁·萨洛宁

38
那不是构造器注入的方式。对象根本不了解IoC容器,也不应该知道。
duffymo

您可以只创建一个空的构造函数,在其中直接调用DI来询问所需的内容。这将删除构造函数madnes,但是您需要确保使用的是DI接口。以防在开发过程中更改DI系统。老实说..即使这是DI注入到您的构造函数中的方法,也没有人会再这样做。doh
Piotr Kula

Answers:


409

正确的是,如果将容器用作服务定位器,则它或多或少是光荣的静态工厂。由于种种原因,我认为这是一种反模式

构造函数注入的奇妙好处之一是,它使违反单一责任原则的行为显而易见。

发生这种情况时,就该重构Facade Services了。简而言之,创建一个新的,更粗粒度的接口,以隐藏您当前需要的一些或所有细粒度依赖项之间的交互。


8
+1,用于将重构工作量化为一个概念;真棒:)
瑞安·埃默尔

46
真的?您刚刚创建了将这些参数移动到另一个类的间接方法,但是它们仍然存在!处理它们只会更复杂。
无可辩驳的2010年

23
@irreputable:在退化的情况下,我们将所有依赖项移到了聚合服务中,我同意这只是间接的另一级别,没有任何好处,因此我选择的单词略有不同。但是,关键是我们只将一些细粒度的依赖项移到了聚合服务中。这限制了新的聚合服务中以及遗留的依存关系的依存关系排列数量。这使得两者都更容易处理。
Mark Seemann 2010年

92
最好的评价是:“构造器注入的一个奇妙好处是,它使违反单一责任原则的行为显而易见。”
伊戈尔·波波夫

2
@DonBox在这种情况下,您可以编写空对象实现来停止递归。不是您所需要的,但重点是构造函数注入并不能防止循环-只能清楚地知道它们在那里。
Mark Seemann

66

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


+1取决于IoC容器,很难在以后不更改所有其他类中的代码束的情况下更改该容器
Tseng 2014年

1
在没有构造函数上的接口参数的情况下,如何实现IOC?我看错你的帖子了吗?
J亨特

@J亨特我不明白你的评论。对我而言,接口参数表示作为依赖项接口的参数,即,如果依赖项注入容器初始化MyClass myClass = new MyClass(IDependency1 interface1, IDependency2 interface2)(接口参数)。这与@derivation的帖子无关,我将其解释为依赖性注入容器不应将自身注入其对象,即MyClass myClass = new MyClass(this)
John Doe

25

传递参数的困难不是问题。问题是您的课程做得太多,应该细分得更多。

依赖注入可以作为类过大的预警,特别是因为传递所有依赖的痛苦越来越大。


43
如果我错了,请纠正我,但是在某些时候,您必须将所有内容“粘合在一起”,因此,您必须获得多个依赖关系。例如,在View层中,当为它们构建模板和数据时,您必须从各种依赖项(例如“服务”)中获取所有数据,然后将所有这些数据放入模板和屏幕中。如果我的网页有10个不同的“块”信息,那么我需要10个不同的类来为我提供这些数据。因此,我的View / Template类需要10个依赖项吗?
安德鲁(Andrew)

4

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

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

带有说明的详细代码可以在这里找到

复杂服务层中IoC的最佳做法


3

我阅读了整个主题两次,并且我认为人们是根据他们所知道的而不是根据所要求的做出回应。

JP最初的问题看起来像是先发送一个解析器,然后再发送一堆类来构造对象,但我们假设这些类/对象本身就是服务,已经可以注入。如果不是,那该怎么办?

JP,如果您希望利用DI 希望将注入与上下文数据混合在一起的荣耀,那么这些模式(或所谓的“反模式”)都不会专门解决这个问题。实际上可以归结为使用一个可以为您提供支持的软件包。

Container.GetSevice<MyClass>(someObject1, someObject2)

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

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

尽管有些框架看起来有些难看,但有些框架可能会有所帮助。例如,Ninject:

使用Ninject在构造函数中使用其他参数创建实例

那是针对.NET的,很流行,仍然没有应有的清晰,但是我敢肯定,无论您选择使用哪种语言,都有一些东西。


3

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

注射不是问题,通常是其他结构缺陷的症状,最明显的是关注点分离。这不是一个问题,但是可以有很多来源,而使它如此难以修复的原因是,您将不得不同时处理所有这些问题(考虑一下意大利面条的纠结)。

这是要注意的事情的不完整清单

域设计不佳(聚合根等)

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

或Mappers(请注意,这些事情可能会导致您遇到麻烦)

查看模型和其他DTO(切勿重复使用,并尽量将其降至最低!!!)


2

问题:

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只是一个原始的方法,如果对此有可靠的论点,那么描述性的评论将不胜感激。


1

这是我使用的方法

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

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

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

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

两种方式都起作用,这是一个优先事项。


21
构造函数注入的原因是使依赖关系显而易见,而不是因为它对Java开发人员而言看起来更自然。
2013年

8
最新评论,但这个答案让我笑了:)
Frederik Prijck

1
+1用于基于setter的注射。如果我在类中定义了服务和存储库,那么它们很明显是依赖关系。很明显,所需字段上的依赖项是什么。
Piotr Kula

从2018年开始,Spring正式建议不要使用setter注入,除非依赖项具有合理的默认值。与之类似,如果对类是强制性的依赖项,则建议构造函数注入。参见关于二传手vs ctor DI的讨论
John Doe
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.