当您还需要“默认”具体依赖项时,哪个是更好的依赖注入模式?
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());
【讨论】:
当然,您会希望能够注入具有不同具体实现的工厂。所以最好定义一个 IFactoryFactory
依赖于MyClass
和ConcreteDependency
,Factory
的任何消费者也隐含地依赖于MyClass
和ConcreteDependency
。虽然使用工厂本身并没有什么问题,但是具体工厂并不能解决紧耦合的问题。
它确实解决了不需要默认构造函数的客户端(MyClass)的紧密耦合问题。当然,你们可以通过使工厂更抽象来走得更远。没关系,但如果我不需要更灵活但更复杂的设计,我会停止使用混凝土工厂。
问题在于,如果要使用上面当前编写的 Factory 类,则无法为 MyClass 注入替代工厂实现。 createDefault() 函数甚至不是虚拟的,因此您不能使用继承来覆盖它。这就是 DI 背后的全部要点:它允许您在不重构代码的情况下更换依赖项的替代具体实现。
鉴于目前的情况,我们不需要这么高的灵活性。我们只需要将MyClass
的两种实例化方式分开到两个不同的地方(MyClass
和Factory
)。因此,虽然MyClass
的代码是纯代码,但想要创建具有默认具体依赖项的MyClass
的客户仍然可以使用工厂完成任务。他们是否需要一个抽象或具体的工厂是另一回事。以上是关于当您还需要“默认”具体依赖项时,哪个是更好的依赖注入模式?的主要内容,如果未能解决你的问题,请参考以下文章
Maven BOM [Bill Of Materials] 依赖
为颤振添加 image_picker 依赖项时出错(iOS)