依赖注入与工厂模式的区别

575
大多数关于依赖注入使用的例子,我们也可以使用工厂模式来解决。看起来在使用/设计时,依赖注入和工厂之间的区别模糊或微小。
有人曾经告诉我,它的不同在于你如何使用它!
我曾经使用StructureMap作为DI容器来解决问题,后来重新设计为使用简单的工厂,并删除了对StructureMap的引用。
有人能告诉我它们之间的区别以及在何处使用什么,这里最好的实践是什么?

27
这两种方法不能互相补充吗:使用依赖注入来注入工厂类? - Richard Ev
21
如果这个问题有一些代码作为答案会很好!我还是不明白依赖注入与使用工厂来创建有什么好处/区别?只需要在工厂类中更改一行代码即可更改创建的对象/实现,这不就可以了吗? - gideon
2
@gideon 那不会强制你编译你的应用程序,或者至少是包含工厂类的模块吗? - lysergic-acid
1
@liortal 是的,没错。自从那条评论以来,我进行了长时间的依赖注入研究,现在我明白 DI 将工厂方法推进了一步。 - gideon
1
看看这个很棒的答案:https://dev59.com/5W445IYBdhLWcg3wNXc_ - 他表达得非常清晰,并提供了代码示例。 - Luis Perez
显示剩余4条评论
31个回答

2
我认为 DI 是一种配置或实例化 bean 的方法。DI 可以通过构造函数、setter-getter 等多种方式进行。
工厂模式只是实例化 bean 的另一种方式。当您使用工厂设计模式创建对象时,主要会使用此模式,因为在使用此模式时,您不会配置 bean 的属性,只会实例化对象。
请查看此链接:Dependency Injection

2

Binoj,

我认为你不必在两者之间做出选择。

将一个依赖类或接口移动到类构造函数或设置器中的行为遵循DI模式。您传递给构造函数或设置器的对象可以使用工厂实现。

何时使用?使用您开发人员熟悉的模式或模式。他们感觉最舒适并且最容易理解的是什么。


1
我认为这些是正交的,可以一起使用。让我给你展示最近在工作中遇到的一个例子:
我们在Java中使用Spring框架进行DI。一个单例类(Parent)必须实例化另一个类(Child)的新对象,并且这些对象有复杂的协作者:
@Component
class Parent {
    // ...
    @Autowired
    Parent(Dep1 dep1, Dep2 dep2, ..., DepN depN) {
        this.dep1 = dep1;
        this.dep2 = dep2;
    }

    void method(int p) {
        Child c = new Child(dep1, dep2, ..., depN, p);
        // ...
    }
}

在这个例子中,Parent 只能接收 DepX 实例并将其传递给 Child 构造函数。存在的问题有:
  1. ParentChild 的了解过多
  2. Parent 拥有过多的协作者
  3. Child 添加依赖需要更改 Parent
这时我意识到一个 Factory 会非常完美地适用于此:
  1. 它隐藏了除了真实参数以外的 Child 类的所有内容,就像 Parent 看到的那样
  2. 它封装了创建一个 Child 的知识,可以集中在 DI 配置中。
这是简化后的 Parent 类和 ChildFactory 类:
@Component
class Parent {
    // ...
    @Autowired
    Parent(ChildFactory childFactory) {
        this.childFactory = childFactory;
    }

    void method(int p) {
        Child c = childFactory.newChild(p);
        // ...
    }
}

@Component
class ChildFactory {
    // ...
    @Autowired
    Parent(Dep1 dep1, Dep2 dep2, ..., DepN depN) {
        this.dep1 = dep1;
        this.dep2 = dep2;
        // ...
        this.depN = depN;
    }

    Child newChild(int p) {
        return new Child(dep1, dep2, ..., depN, p);
    }
}

1

工厂模式

工厂设计模式以以下特点为标志:

  • 一个接口
  • 实现类
  • 一个工厂

当你自问以下问题时,可以观察到一些事情:

  • 工厂会在运行时还是编译时为实现类创建对象? - 运行时
  • 如果你想在运行时切换实现怎么办? - 不可能

这些问题通过依赖注入来解决。

依赖注入

有不同的方式来注入依赖项。简单起见,让我们选择接口注入。

在DI中,容器创建所需的实例,并将其“注入”到对象中。

因此,静态实例化被消除。

例子:

public class MyClass{

  MyInterface find= null;

  //Constructor- During the object instantiation

  public MyClass(MyInterface myInterface ) {

       find = myInterface ;
  }

  public void myMethod(){

       find.doSomething();

  }
}

1
从外表上看,它们看起来相同。
简单地说,工厂模式是一种创建模式,可以帮助我们创建一个对象 - “定义创建对象的接口”。如果我们有一个键值排序的对象池(例如字典),将键传递给工厂(我指的是简单工厂模式),您可以解析类型。任务完成!而依赖注入框架(例如Structure Map、Ninject、Unity等)则似乎正在做同样的事情。
但是...“不要重复造轮子”。
从架构角度来看,它是一个绑定层和“不要重复造轮子”。
对于企业级应用程序,DI的概念更多地是一个定义依赖关系的架构层。为了进一步简化这个概念,您可以将其视为一个独立的类库项目,用于进行依赖项解析。主应用程序依赖于此项目,其中依赖项解析器引用其他具体实现以及依赖项解析。
除了从工厂中获取/创建之外,我们通常需要更多的功能(例如使用XML定义依赖项、模拟和单元测试等)。由于您提到了Structure Map,请查看Structure Map功能列表。它显然不仅仅是解决简单的对象映射。不要重复造轮子! 如果你只有一把锤子,那么所有东西看起来都像钉子
根据您的需求和构建的应用程序类型,您需要做出选择。如果它只有几个项目(可能只有一个或两个...)并涉及少量依赖项,则可以选择更简单的方法。这就像在进行简单的1或2个数据库调用时使用ADO .Net数据访问,其中在该场景中引入EF是过度设计。
但对于一个较大的项目或者如果您的项目变得更加庞大,我强烈建议使用带有框架的DI层,并留有更改您使用的DI框架的空间(在主应用程序(Web应用程序、Web Api、桌面等)中使用Facade)。

1
注入框架是工厂模式的实现。
这完全取决于您的需求。如果您需要在应用程序中实现工厂模式,很可能您的需求会被众多注入框架实现之一满足。
仅当第三方框架无法满足您的需求时,才应该自己开发解决方案。您编写的代码越多,维护的代码也就越多。代码是负债而不是资产。
关于使用哪种实现进行争论并不像理解应用程序的架构需求那样基本重要。

0
工厂模式帮助我们决定需要创建哪个对象,而依赖注入则帮助我们注入具体对象。
两者是不同的。我们可以根据工厂模式决定返回哪个对象。我们可以使用依赖注入来返回对象,而不是使用new来创建对象。这样,工厂模式就不会违反依赖注入。

0

DI(依赖注入)为您提供组合根,这是一个单一的集中位置,用于连接您的对象图。这往往使对象依赖关系非常明确,因为对象只请求它们需要的内容,并且只有一个地方可以获取它。

组合根是一种干净而直接的关注点分离。被注入的对象不应该依赖于 DI 机制,无论是第三方容器还是 DIY DI。DI 应该是不可见的。

工厂往往更加分散。不同的对象使用不同的工厂,而工厂代表了对象和它们实际依赖之间的另一层间接性。这个额外的层次会向对象图添加它自己的依赖关系。工厂是不可见的。工厂是中间人。

因此,更新工厂更具问题性:由于工厂是业务逻辑的依赖项,修改它们可能会产生连锁反应。组合根不是业务逻辑的依赖项,因此可以在隔离环境中进行修改。

GoF提到了更新抽象工厂的困难。他们的解释部分在这里引用here。将DI与工厂进行对比,也与问题Is ServiceLocator an anti-pattern?有很多共同之处。

最终,选择哪个可能是有主观看法的;但我认为它归结为工厂是一个中间人。问题在于,这个中间人是否通过添加除了提供产品以外的附加价值来发挥其作用。因为如果你可以在没有中间人的情况下获得同样的产品,那么为什么不削减中间人呢?

一张图表可以帮助说明区别。 DI vs Factory


0
[Factory] -> 有一个类可以基于请求参数创建其他类。在现实世界中,“工厂”也为您制造“对象”。您可以要求您的汽车供应商工厂(免费 :))为您制造特斯拉版本1。
[DI] -> 有一个(服务)容器可以存储接口(契约类)。您不需要关心如何创建对象。您只需向某个人/地方请求实现,细节和其他内容对调用者或消费者不重要。
DI是SOLID原则中“D”的基础。

-1

我使用两者来创建一个更易读的控制反转策略,以便开发人员在我之后需要维护它。

我使用工厂来创建不同的层对象(业务逻辑、数据访问)。

ICarBusiness carBusiness = BusinessFactory.CreateCarBusiness();

另一个开发人员将看到此内容,当创建业务层对象时,他会查看BusinessFactory和Intellisense会向开发人员提供所有可能创建的业务层。不必玩游戏,找到我想要创建的接口。

这个结构已经是控制反转了。我不再负责创建特定的对象。但是您仍然需要确保依赖注入以便轻松更改事物。创建自己的自定义依赖注入是荒谬的,所以我使用Unity。在CreateCarBusiness()中,我请求Unity解析哪个类属于此类及其生命周期。

因此,我的代码工厂依赖注入结构如下:

public static class BusinessFactory
{
    public static ICarBusiness CreateCarBusiness()
    {
       return Container.Resolve<ICarBusiness>();
    }
}

现在我同时获益于两者。我的代码对其他开发者来说更易读,因为我使用对象范围而不是构造函数依赖注入,后者只是表示在类创建时每个对象都可用。
当我创建单元测试时,我使用这个方法将我的数据库数据访问更改为自定义编码的数据访问层。我不想让我的单元测试与数据库、Web服务器、电子邮件服务器等通信。它们需要测试我的业务层,因为那里是智能的地方。

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