如何避免依赖注入构造函数的混乱?

358

我发现我的构造函数越来越长了:

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

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

public MyClass(Container con)

每个类都要使用控制反转(IoC)和依赖注入(DI)吗?这样做有什么缺点吗?如果我这样做,感觉就像在使用一个过于复杂的静态工具。请分享您对IoC和DI热潮的想法。


70
你为什么要传递容器?我认为你可能误解了控制反转(IOC)。 - Paul Creasey
39
如果你的构造函数需要越来越多的参数,那么可能是因为你在这些类中做了太多的事情。 - Austin Salonen
41
这不是构造函数注入的正确方式。对象完全不知道IoC容器,也不应该知道。 - duffymo
你可以创建一个空构造函数,在其中直接调用 DI,请求所需的内容。这将消除构造函数的疯狂,但您需要确保使用 DI 接口..以防在开发过程中更改 DI 系统。老实说..没有人会回到这种方式,即使这就是 DI 注入到构造函数中的方式。咳 - Piotr Kula
10个回答

478

您说得对,如果将容器用作服务定位器,它基本上就是一个不折不扣的静态工厂。出于许多原因,我认为这是一种反模式(请参见我书中的此处摘录)。

构造函数注入的一个美妙好处是,它使单一职责原则的违反非常明显。

当出现这种情况时,就是进行外观服务重构的时候了。简而言之,创建一个新的、更粗粒度的接口,隐藏当前所需的某些或所有细粒度依赖项之间的交互。


10
感谢肯定重构工作量的量化方式,非常棒 :) - Ryan Emerle
53
真的吗?你只是创造了一个间接方法将这些参数移动到另一个类中,但它们仍然存在!只是更加复杂,难以处理它们。 - irreputable
28
在把所有的依赖关系都移动到聚合服务的退化情况下,我同意这只是另一种没有任何好处的间接级别,因此我的措辞有点不准确。然而,重点是仅将部分细粒度的依赖关系移动到聚合服务中。这限制了新聚合服务和留下的依赖关系中依赖关系排列的数量。这使得它们更容易处理。 - Mark Seemann
109
最佳评价:“构造器注入的一个奇妙好处是,它能够让单一职责原则的违反变得非常明显。” - Igor Popov
2
@DonBox 在这种情况下,您可以编写空对象实现来停止递归。虽然不是您所需的,但重点是构造函数注入并不能防止循环 - 它只能清楚地表明它们存在。 - Mark Seemann
显示剩余3条评论

71

我认为您的类构造函数不应该直接引用IOC容器。这样会导致类与容器之间存在不必要的依赖关系(而IOC正是试图避免这种类型的依赖关系!)。


1
你如何在构造函数中没有接口参数的情况下实现IOC?我是否误读了你的帖子? - J Hunt
@J Hunt,我不理解你的评论。对我来说,接口参数意味着作为依赖项接口的参数,即如果您的依赖注入容器初始化了MyClass myClass = new MyClass(IDependency1 interface1, IDependency2 interface2)(接口参数)。这与@derivation的帖子无关,我理解为依赖注入容器不应将自身注入其对象中,即MyClass myClass = new MyClass(this) - John Doe
我无法理解在某些情况下如何避免传递IoC容器。例如(可能是唯一有效的):工厂。class MyTypeFactory { private readonly IServiceProvier mServices; public MyTypeFactory(IServiceProvier services) => mServices = services; MyType Create(int param) => ActivatorUtilities.CreateInstance(mServices, param); } - Mike
这个回答如何解决问题? - mr5
@Mike - ActivatorUtilities 是您可以避免传递容器的方法。请参见此处的答案:https://dev59.com/OHUOtIcB2Jgan1znv2u6#72482813 - Shai Cohen
显示剩余2条评论

34

传递参数的难度不是问题,问题在于你的类功能过于多,需要进一步拆分。

依赖注入可以作为一个早期警报,用于防止类变得过于庞大,特别是因为要传递所有依赖项而导致的越来越大的麻烦。


57
请纠正我,但在某个时候您必须“将所有内容粘合在一起”,因此您必须获取多个依赖项。例如,在构建模板和数据时,您需要从各种依赖项(例如“服务”)中获取所有数据,然后将所有这些数据放入模板并呈现到屏幕上。如果我的网页有10个不同的信息块,则需要10个不同的类来为我提供该数据。所以,我需要将这10个依赖项注入到我的View/Template类中吗? - Andrew
1
安德鲁,你的网页可能做了太多的事情。你可能需要重新考虑网页的设计,是否应该将其拆分为多个页面或单独注入各种组件。首先,网页的性能可能非常糟糕。 - JPA

5

问题:

1) 构造函数的参数列表逐渐增加。

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

解决方案 1

IoC容器传递给构造函数

为什么

  • 不再有无止境的参数列表
  • 构造函数签名变得简单

为什么不

  • 使您的类与IoC容器紧密耦合。 (当您要在使用其他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容器相同
  • ..

解决方案2只是一个初步的想法,如果有充分的反对意见,则欢迎提供描述性评论。


解析器将在构造函数中进行依赖注入数据上下文和接口。 - Golden Lion

4

我已经仔细阅读了这个帖子两次,我认为人们的回应是基于他们所知道的而不是所问的。

JP最初的问题似乎是通过发送解析器和一堆类来构建对象,但我们假设这些类/对象本身就是服务,可以进行注入。那么如果它们不是呢?

JP,如果你想利用DI并且想要将注入与上下文数据混合使用,那么这些模式(或所谓的“反模式”)都没有特别涉及到。实际上,这归结为使用一个支持此类尝试的软件包。

Container.GetSevice<MyClass>(someObject1, someObject2)

……很少有支持这种格式的。我认为编写此类支持程序的困难程度,加上与实现相关的可怜性能,使其对开源开发人员不具吸引力。

但是应该这样做,因为我应该能够创建并注册MyClass的工厂,并且该工厂应该能够接收不必为传递数据/模型而存在的“服务”的数据/输入。如果“反模式”是指负面后果,则强制存在用于传递数据/模型的人造服务类型肯定是负面的(与将类包装到容器中的感觉相同。同样的本能也适用)。

尽管它们看起来有点丑陋,但有些框架可能会有所帮助。例如,Ninject:

使用具有构造函数中附加参数的Ninject创建实例

这是针对.NET的,很受欢迎,但仍然不如应该,但我敢肯定无论您选择使用哪种语言,都会有一些东西可以帮助你。


4
我遇到了一个类似的问题,关于基于构造函数的依赖注入,传递所有依赖项变得非常复杂。过去我使用的一种方法是使用应用程序门面模式,使用服务层。这将具有粗略的API。如果该服务依赖于存储库,则会使用私有属性的setter注入。这需要创建一个抽象工厂,并将创建存储库的逻辑移动到工厂中。详细的代码和说明可以在此找到: IoC在复杂服务层中的最佳实践

3

这是我使用的方法

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


3
注入容器只是一种捷径,最终你会后悔的。
过度注入不是问题,通常是其他结构缺陷的症状,尤其是关注点分离。这不是一个问题,而可能有多个来源,使问题变得更加困难的是,你需要同时处理它们(想象解开一堆意大利面)。
以下是需要注意的事项的不完整列表:
- 糟糕的领域设计(聚合根等) - 关注点分离不足(服务组合、命令、查询),请参考CQRS和事件溯源。 - ORM映射器(小心,这些东西可能会让你陷入麻烦) - 视图模型和其他DTO(永远不要重用一个,并尽量将其保持到最小!!!)

1

一个方法有太多参数可能是一个提示(不一定)你的方法太大/责任过多。

此外,在同一个方法中使用的参数(可能)具有高内聚性 -> 值得考虑将它们放入一个类中 -> 只需要传递一个参数。


-7
你使用的依赖注入框架是什么?你尝试过使用基于setter的注入吗?
构造函数注入的好处是对于不使用DI框架的Java程序员来说,它看起来很自然。你需要5个东西来初始化一个类,然后你的构造函数有5个参数。缺点是当你有很多依赖关系时,它变得笨重。
使用Spring,你可以通过setter传递所需的值,并使用@required注释强制执行它们被注入。缺点是你需要将初始化代码从构造函数移动到另一个方法,并通过使用@PostConstruct标记让Spring在所有依赖项注入后调用它。我不确定其他框架是否做类似的事情。
两种方式都可以工作,这只是个人喜好的问题。

26
构造函数注入的原因是为了让依赖关系更加明显,而不是因为它看起来更符合Java开发人员的习惯。 - L-Four
8
晚来的评论,但这个答案让我笑了 :) - Frederik Prijck
1
基于setter的注入方式很棒。如果我在类中定义了服务和存储库,它们是依赖项非常明显...我不需要编写大量VB6样式的构造函数,并在构造函数中执行愚蠢的分配代码。所需字段的依赖关系非常明显。 - Piotr Kula
截至2018年,Spring官方建议除了那些有合理默认值的依赖项外,不再使用setter注入。也就是说,如果依赖项对于类来说是必需的,则推荐使用构造函数注入。请参见关于setter和ctor DI的讨论 - John Doe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接