当您还需要“默认”具体依赖项时,哪个是更好的依赖注入模式?

Posted

技术标签:

【中文标题】当您还需要“默认”具体依赖项时,哪个是更好的依赖注入模式?【英文标题】:Which is a better pattern for dependency injection when you also want a "default" concrete dependency? 【发布时间】:2019-04-12 14:34:36 【问题描述】:

为了可测试性,我使用了以下两种模式,但我想知道哪个是更好的 OOP/SOLID。

模式一:提供两个构造函数,其中一个创建具体依赖

public MyClass

    private IDependency dependency;

    public MyClass() : this(new ConcreteDependency())  

    public MyClass(IDependency dependency)
    
       this.dependency = dependency;
    

模式2:基类有依赖初始化器,派生类用具体依赖调用它

public abstract MyClassBase

    private IDependency dependency;

    protected void Initialize(IDependency dependency)
    
        this.dependency = dependency;
    



public MyClass : MyClassBase

    public MyClass()
    
       Initialize(new ConcreteDependency());
    

【问题讨论】:

我都不建议。如果您使用的是 DI,那么我将删除您上面所有缺少参数的构造函数。如果可能,您的课程根本不应该提及ConcreteDependency。关于ConcreteDependency唯一应该知道的是你的 IoC 容器。 您还提到了可测试性。在这种情况下,您的单元测试应该负责注入具体的类(或 mocks 进行更多与实现无关的测试) @NPras 单元测试将是 Pattern1 中的第二个构造函数或 Pattern 2 中的基类 你能告诉我们为什么你没有按照我在第一条评论中的建议去做吗?为什么你觉得需要两个构造函数? 【参考方案1】:

提供的两个示例都展示了 Control Freak 反模式,如 Dependency Injection, Second Edition Principles, Practices, and Patterns 中所述。

Control Freak 反模式出现:

每次您在Composition Root 以外的任何地方依赖易失性依赖。这违反了Dependency Inversion Principle

在第一个示例中,即使 MyClass 使用了 IDependency 抽象,它也会拖着对具体 ConcreteDependency 组件的引用,导致两个类紧密耦合

在您的第二个示例中发生了同样的紧密耦合,这实际上是相同的反模式。此外,第二个示例甚至使用Initialize 方法将依赖项应用于已创建的MyClassBase 实例。这会导致Temporal Coupling,这本身就是一种代码异味。

适当的解决方案可以防止导致紧密耦合和时间耦合,这意味着您使用构造函数注入只定义一个构造函数

public MyClass

    private IDependency dependency;

    public MyClass(IDependency dependency)
    
       this.dependency = dependency ?? throw new ArgumentNullException("dependency");
    

【讨论】:

这似乎没有解决问题的后半部分,“...当您还想要一个默认的具体依赖项时? @jaco0646:确实如此,因为答案仍然相同。您可以通过消费者的单个公共构造函数注入“默认”依赖项,就像使用任何其他依赖项一样。 @Steven 假设依赖是IHttpClient。几乎所有使用我课程的人都会通过new HttpClient()。现在我确定HttpClient 有一些依赖关系,比如ISomething,所以创建HttpClient 的每个人都会做new HttpClient(new ConcreteSomething()),它变成了一个无限的具体事物系列,有人需要创建才能使用我的类。 如果MyClass 需要IHttpClient,则通过其ctor 传递该依赖项。 MyClass 的消费者不应创建 MyClass,而是将其注入到他们的 ctor 中。这将创建此类的责任推到了应用程序中的一个位置,称为Composition Root。这意味着只有一个位置,并且可能一个代码块创建new MyClass(new HttpClient(new ConcreteSomething())。它可以防止代码重复并最大限度地提高灵活性和可维护性。 并非每次我们都想最大限度地提高灵活性。灵活地伴随着成本:复杂性。我会从简单的设计开始,并在需要时逐渐增加更多的灵活性/复杂性。是的,知道如何创建灵活的设计非常重要,但知道如何做出权衡更为重要。【参考方案2】:

您的两种模式的缺点是它们可能会破坏Dependency Inversion Principle:MyClass 是一个更抽象的东西,但知道ConcreteDependency 这是一个更具体的东西。确实:

    无论在何处重复使用 MyClass 类,ConcreteDependency 类都必须可用。 另外,如果你需要将ConcreteDependency更改为ConcreteDependency2,那么你的MyClass类必须更改,这在某些情况下是不好的,例如:有MyClass的客户从不使用它默认构造函数。

更不用说,模式 #2 可能会使您的对象处于意外状态,因为派生类可能会忘记调用 Initialize 方法,除非您的文档非常好。

我建议使用工厂模式:

class Factory 
    public MyClass createDefault() 
        return new MyClass(new ConcreteDependency());
    

【讨论】:

当然,您会希望能够注入具有不同具体实现的工厂。所以最好定义一个 IFactory 或 IMyClassFactory 接口。 我同意@RyanPierceWilliams。您的回答一开始就很强,但是您的 Concrete Factory 解决方案仍然会导致强耦合。这是由于依赖关系是可传递的。这意味着由于Factory 依赖于MyClassConcreteDependencyFactory 的任何消费者也隐含地依赖于MyClassConcreteDependency。虽然使用工厂本身并没有什么问题,但是具体工厂并不能解决紧耦合的问题。 它确实解决了不需要默认构造函数的客户端(MyClass)的紧密耦合问题。当然,你们可以通过使工厂更抽象来走得更远。没关系,但如果我不需要更灵活但更复杂的设计,我会停止使用混凝土工厂。 问题在于,如果要使用上面当前编写的 Factory 类,则无法为 MyClass 注入替代工厂实现。 createDefault() 函数甚至不是虚拟的,因此您不能使用继承来覆盖它。这就是 DI 背后的全部要点:它允许您在不重构代码的情况下更换依赖项的替代具体实现。 鉴于目前的情况,我们不需要这么高的灵活性。我们只需要将MyClass 的两种实例化方式分开到两个不同的地方(MyClassFactory)。因此,虽然MyClass 的代码是纯代码,但想要创建具有默认具体依赖项的MyClass 的客户仍然可以使用工厂完成任务。他们是否需要一个抽象或具体的工厂是另一回事。

以上是关于当您还需要“默认”具体依赖项时,哪个是更好的依赖注入模式?的主要内容,如果未能解决你的问题,请参考以下文章

GO语言(二十五):管理依赖项(上)-

当它是另一个项目的依赖项时,捆绑我的汇总项目是什么

Maven BOM [Bill Of Materials] 依赖

为颤振添加 image_picker 依赖项时出错(iOS)

Maven + SLF4J:使用需要两个不同 SLF4J 版本的两个不同依赖项时的版本冲突

当它具有也是 aar 库的依赖项时,如何为 android 库生成 javadoc?