ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

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

public MyClass(Container con)

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

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

S
Steven

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

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

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


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

我认为您的类构造函数不应该引用您的 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); }
这如何回答这个问题?
k
kyoryu

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

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


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

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

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

可以在这里找到带有解释的详细代码

Best practices for IoC in complex service layer


t
tchelidze

问题 :

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 只是一个原始的,如果有充分的论据反对它,那么描述性评论将不胜感激


解析器将依赖注入构造函数中的数据上下文和接口。
C
Craig Brunetti

我把这整篇文章读了两遍,我认为人们是根据他们所知道的而不是被问到的来回应的。

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

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

Container.GetSevice<MyClass>(someObject1, someObject2)

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

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

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

Creating an instance using Ninject with additional parameters in the constructor

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


M
Marius

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

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

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

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

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

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

查看模型和其他 DTO(永远不要重复使用一个,并尽量将它们保持在最低限度!!!!)


R
Ronijo

这是我使用的方法

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


D
David W Crook

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

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

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

两种方式都有效,这是一个偏好问题。


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