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

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个回答

322
使用工厂模式时,实际上仍然由您的代码负责创建对象。通过依赖注入,您可以将该责任外包给另一个类或框架,这与您的代码是分离的。

196
依赖注入模式不需要任何框架,您可以通过手动编写执行依赖注入的工厂来实现依赖注入。依赖注入框架只是让这个过程更容易而已。 - Esko Luontola
36
@Perpetualcoder - 谢谢 @Esko - 不要被“框架”这个词误导,它并不意味着一些高级的第三方库。 - willcodejavaforfood
4
+1 @willcode 谢谢!所以你必须更改配置/XML文件。我明白了。维基链接http://en.wikipedia.org/wiki/Dependency_injection#Manually-injected_dependency将工厂模式定义为“手动注入依赖项”。 - gideon
5
请问您需要将以下内容翻译为中文吗?我不明白修改一个XML行与修改一行代码相比的优势。您可以详细解释一下吗? - Rafael Eyng
2
重复OP的答案,将“DI”替换为“工厂”,仍然有意义。 - Edson Medina
显示剩余6条评论

244

我建议保持概念的简单明了。依赖注入更多地是一种用于松耦合软件组件的架构模式。工厂模式只是将创建其他类对象的责任分离到另一个实体的一种方式。工厂模式可以被称为实现DI的一种工具。依赖注入可以通过使用构造函数、使用映射XML文件等多种方式来实现。


5
工厂模式确实是实现依赖注入的一种方法之一。使用依赖注入相对于工厂模式的另一个好处是,依赖注入框架可以让您灵活地注册抽象和具体类型之间的关系,例如代码配置、XML或自动配置等。这种灵活性将使您能够管理对象的生命周期或决定何时以及如何注册您的接口。 - Teoman shipahi
3
工厂模式比依赖注入提供了更高级别的抽象。工厂可以像依赖注入一样提供配置选项,但它还可以选择隐藏所有这些配置细节,以便工厂可以决定在客户端不知情的情况下切换到完全不同的实现。依赖注入本身不允许这样做。依赖注入要求客户端指定他想要的更改。 - neuron
1
不错的回答。许多人对这个问题感到困惑:“我应该选择哪些模式?”实际上,设计模式只是在适当的情况下解决问题的一种方式。如果你不知道应该使用什么设计模式,或者“我应该在哪里实现这个模式?”,那么你现在不需要它。 - Benyi
4
我认为更准确地说,工厂模式是实现控制反转(IoC)的一种工具(而不是依赖注入(DI))。请注意,DI 是 IoC 的一种形式 - Hooman Bahreini
@neuron,这是唯一一种情况,当已经有 DI 容器可用时,手动实现工厂才是合理的用例:当您想要隐藏调用者的创建配置细节时。除此之外,DI 容器可以完美地作为工厂使用,并节省您手动实现的工作量。 - Lesair Valmont

240

依赖注入

汽车不再自行实例化部件,而是请求需要的部件以使其正常工作。

class Car
{
    private Engine engine;
    private SteeringWheel wheel;
    private Tires tires;

    public Car(Engine engine, SteeringWheel wheel, Tires tires)
    {
        this.engine = engine;
        this.wheel = wheel;
        this.tires = tires;
    }
}

工厂模式

将各个部分组合在一起,形成一个完整的对象,并且对调用者隐藏了具体类型。

static class CarFactory
{
    public ICar BuildCar()
    {
        Engine engine = new Engine();
        SteeringWheel steeringWheel = new SteeringWheel();
        Tires tires = new Tires();
        ICar car = new RaceCar(engine, steeringWheel, tires);
        return car;
    }   
}

结果

正如你所看到的,工厂和依赖注入相互补充。

static void Main()
{
     ICar car = CarFactory.BuildCar();
     // use car
}

你还记得《金发姑娘和三只熊》这个故事吗?其实依赖注入也有点像那样。以下是三种做同一件事的方法。

void RaceCar() // example #1
{
    ICar car = CarFactory.BuildCar();
    car.Race();
}

void RaceCar(ICarFactory carFactory) // example #2
{
    ICar car = carFactory.BuildCar();
    car.Race();
}

void RaceCar(ICar car) // example #3
{
    car.Race();
}

示例 #1 - 这是最糟糕的,因为它完全隐藏了依赖关系。如果你把这个方法看做黑盒,你就不知道它需要一辆车。

示例 #2 - 这比较好,因为现在我们知道需要一辆车,因为我们传入了一个汽车工厂。但这次我们传递了太多信息,因为实际上这个方法只需要一辆车。我们传入工厂只是为了在方法内部建造汽车,而这辆汽车可以在方法外部建造并传递进来。

示例 #3 - 这是理想的,因为该方法请求 恰好 需要的东西,既不多也不少。我不需要编写 MockCarFactory 来创建 MockCars,我可以直接传递模拟对象。它是直接的,接口没有欺骗性。

这个由 Misko Hevery 做的 Google Tech Talk 很棒,它是我从中得出示例的基础。http://www.youtube.com/watch?v=XcT4yYu_TTs


2
工厂模式作为依赖注入被注入。 - Mangesh
11
您所描述的更像是服务定位器模式。它们有着相似的目标,即将服务与其消费者解耦。然而,服务定位器模式存在许多缺点。主要的问题在于隐藏依赖关系不是一件好事。消费者仍然需要获取它们的依赖项,但是这些依赖项被“秘密地”传递并依赖全局状态,而不是定义一个清晰的接口。在这个例子中,消费者不知道工厂的存在,它只是请求自己所需要的东西,而不必关心如何构建这个需求。 - Despertar
2
@MatthewWhited 大多数设计决策都需要权衡。使用 DI,您可以获得灵活性、透明度和更易于测试的系统,但代价是暴露了您的内部细节。许多人认为这是一个可以接受的权衡。这并不意味着 DI 总是最好的解决方案,本问题也不涉及此类问题。该示例旨在通过展示每种模式的一些良好和不良用法来展示 DI 和工厂模式之间的区别。 - Despertar
4
这不是一个依赖注入模式,这只是控制反转的一种实现方式。依赖注入使用服务定位器来确定正确的依赖关系,并在对象创建期间将其注入到构造函数中,而您的代码只是将对象的创建与类范围分离。 - Diego Mendes
11
您所描述的是一个自动化依赖注入的框架。您所称的ServiceLocator执行了依赖注入的操作。而我展示的是这个模式本身,它不需要任何花哨的框架。请参见https://dev59.com/Y3VC5IYBdhLWcg3w-WSs#140655,或参见https://en.wikipedia.org/wiki/Dependency_injection#Dependency_injection_frameworks:“[框架列表]支持依赖注入,但不是必须进行依赖注入”。此外,“注入是将依赖项传递给相关对象”。您正在使一个非常简单的概念变得过于复杂。 - Despertar
显示剩余7条评论

83
依赖注入(DI)和工厂模式之所以相似,是因为它们都是反转控制(IoC)的两种实现方式,而IoC是一种软件架构。简单来说,它们是解决同一问题的两种方法。
所以回答问题,工厂模式和DI的主要区别在于对象引用的获取方式。依赖注入顾名思义,引用被注入或提供给您的代码。而工厂模式则需要您的代码请求引用,因此您的代码获取对象。这两种实现方式均可消除代码与底层类或对象引用类型之间的联系或解耦。
值得注意的是,工厂模式(甚至包括返回新工厂以返回对象引用的抽象工厂模式)可以编写动态选择或链接在运行时请求的对象类型或类的代码。这使它们与服务定位器模式(另一种IoC的实现方式)非常相似(比DI还要相似)。
工厂设计模式相当古老(就软件而言),已经存在一段时间了。随着IoC架构模式的近期流行,它正在重新兴起。
我想,当涉及到IoC设计模式时:注入者应该注入,定位器应该定位,而工厂已经进行了重构。

8
这是最佳答案......其他答案要么未提及IoC,要么未认识到DI是IoC的一种形式。 - Hooman Bahreini
谢谢,当我第一次学习IOC时,我几乎尖叫着说这只是工厂模式的另一种形式,结果它们确实非常相似。 - Harvey Lin
Tom,我想要冷静一下,所以来问一个问题,但是发现已经有人问过了。我的观点是,与其使用和担心FP,我是否可以几乎所有时候都使用DI来解耦类并实现开闭原则。对我而言,通过DI中的服务类所能实现的,也可以通过FP来实现,但我会选择DI方法,因为我看到一些示例,在工厂模式的基础上还实现了DI,使用ServiceCollection返回IServiceProvider或手动创建服务。那么,为什么还要费心去使用FP呢?你认为呢? - Faisal
2
@Faisal 我认为这是一个不错的方法。DI是现代框架(如ASP.NET Core)中首选的技术。正如之前提到的,永远不会有一种适用于所有情况的解决方案。总会有一些使用情况需要以某种方式使用工厂模式。而且,正如你所看到的,大多数DI框架都支持以某种格式使用工厂模式。因此,如果您确实需要它们,它们就是扩展点。 - Tom Maher

40

在某些情况下,使用依赖注入可以轻松解决问题,而使用一套工厂可能就不那么容易了。

控制反转和依赖注入(IOC/DI)与服务定位器或一套工厂(Factory)之间的区别在于:

IOC/DI本身是一个完整的领域对象和服务生态系统。它按照您指定的方式为您设置所有内容。您的领域对象和服务由容器构建,而不是自己构建:因此它们没有任何对容器或任何工厂的依赖性。IOC/DI允许高度可配置性,在应用程序的最上层(GUI、Web前端)的单个位置(容器的构建)中进行所有配置。

而工厂抽象出一些领域对象和服务的构造过程。但领域对象和服务仍然需要自行确定如何构建自己以及如何获取它们所依赖的所有内容。所有这些“主动”依赖关系会贯穿您应用程序的所有层。因此,没有单个位置可以去配置所有内容。


32

依赖注入的一个缺点是无法使用逻辑来初始化对象。例如,当我需要创建一个具有随机名称和年龄的角色时,依赖注入不如工厂模式。通过工厂模式,我们可以轻松地将随机算法从对象创建中封装起来,这支持了名为“封装变化”的一种设计模式。


24
生命周期管理是依赖容器除了实例化和注入之外的职责之一。容器有时会在实例化后保留对组件的引用,这就是它被称为“容器”而不是“工厂”的原因。依赖注入容器通常只保留需要管理生命周期的对象的引用,或者是像单例或享元这样将来要重复使用的对象。当配置为为每个对容器的调用创建某些组件的新实例时,容器通常会忘记已创建的对象。
来源:http://tutorials.jenkov.com/dependency-injection/dependency-injection-containers.html

21

理论

需要考虑两个重要点:

  1. 谁创建对象:
  • [工厂模式]:您必须编写对象的创建方式。您有单独的工厂类,其中包含创建逻辑。
  • [依赖注入]:在实际情况中,这由外部框架完成(例如,在Java中,这将是spring/ejb/guice)。注入会“神奇地”发生,而不需要显式创建新对象。
  1. 它管理哪种类型的对象:
  • [工厂模式]:通常负责创建有状态的对象
  • [依赖注入]:更可能创建无状态的对象

如何在单个项目中同时使用工厂模式和依赖注入的实际示例

  1. 我们想要构建什么

用于创建包含多个订单行条目的订单的应用程序模块。

  1. 架构

假设我们想要创建以下分层架构:

enter image description here

域对象可能是存储在数据库中的对象。 Repository(DAO)有助于从数据库中检索对象。 Service为其他模块提供API。允许对order模块进行操作。

  1. 领域层和工厂的使用

将在数据库中的实体是Order和OrderLine。订单可以有多个OrderLines。 Relationship between Order and OrderLine

现在来到重要的设计部分。模块外部的类是否应该自己创建和管理OrderLines?不应该。只有当您与订单相关联时,Order Line才应存在。最好隐藏内部实现以外部类。

但是如何在不了解OrderLines的情况下创建订单呢?

工厂模式

想要创建新订单的人使用OrderFactory(它会隐藏关于如何创建订单的细节)。

输入图像描述

这是在IDE内部的样子。位于domain包之外的类将使用OrderFactory而不是Order内部的构造函数。

  1. 依赖注入 依赖注入更常用于无状态层,例如存储库和服务。

OrderRepository和OrderService由依赖注入框架管理。 Repository负责管理数据库上的CRUD操作。 Service注入Repository并使用它来保存/查找正确的domain类。

输入图像描述


你能否请澄清一下这个问题? [工厂]:通常负责创建有状态的对象 [依赖注入] 更可能创建无状态的对象 我不明白为什么。 - Maksim Dmitriev
2
有状态的对象通常位于应用程序的领域层,您将数据映射到数据库时通常不会在那里使用依赖注入。 工厂经常用于从复杂对象树中创建AggregateRoot。 - Marcin Szymczak

16

我认为DI是工厂的一种抽象层,但它们提供了超越抽象的好处。一个真正的工厂知道如何实例化一个单一类型并进行配置。一个良好的DI层通过配置提供了能力,可以实例化和配置许多类型。

显然,对于一个包含几个简单类型的项目,在它们的构建中需要相对稳定的业务逻辑,工厂模式易于理解、实现并且效果良好。

另一方面,如果您有一个包含许多类型的项目,它们的实现经常会发生变化,DI通过其配置为您提供了灵活性,无需重新编译您的工厂即可在运行时进行更改。


14

我知道这个问题很老,但我想要补充我的意见,

我认为依赖注入(DI)在许多方面类似于可配置的工厂模式(FP),在这种意义上,您可以使用工厂完成DI所能完成的任何操作。

事实上,如果您使用Spring,您可以选择自动装配资源(DI),或者像这样做:

MyBean mb = ctx.getBean("myBean");

然后使用该“mb”实例来执行任何操作。这不是一个调用返回实例的工厂吗?

大多数FP示例之间唯一真正的区别在于,您可以在xml或另一个类中配置“myBean”,并且框架将作为工厂工作,但除此之外,它是相同的事情,并且您肯定可以拥有读取配置文件或根据需要获取实现的工厂。

如果你问我的意见(我知道你没有),我认为DI做的事情是一样的,但只是增加了开发的复杂性,为什么?

好吧,首先,要想知道您使用DI自动装配的任何bean的实现是什么,您必须转到配置本身。

但是...那个承诺说您不必了解正在使用的对象的实现呢?咳!真的吗?当您使用这种方法时...难道您不是编写实现的人吗?即使您不是,您几乎始终都在看实现是如何完成其工作的吧?

还有最后一件事,无论DI框架承诺多少能够从中构建与其分离、无依赖于其类的东西,如果您使用框架,则会在其周围构建所有内容,如果您必须更改方法或框架将不是一个容易的任务...永远!...但是,既然您在特定框架周围构建了所有内容,而不是担心什么是最好的业务解决方案,那么当您这样做时,将会面临一个大问题。

事实上,我能看到FP或DI方法的唯一真正商业应用是如果您需要在运行时更改正在使用的实现,但至少我知道的框架不允许您这样做,您必须在开发期间将所有内容配置好,如果您需要使用它,请使用另一种方法。

因此,如果我有一个在同一应用程序的两个范围内执行不同操作(比如说,控股公司的两个公司)的类,我必须配置该框架创建两个不同的bean,并调整我的代码以使用每个bean。难道这不和我写下类似这样的东西一样吗:

MyBean mb = MyBeanForEntreprise1(); //In the classes of the first enterprise
MyBean mb = MyBeanForEntreprise2(); //In the classes of the second enterprise

和这个一样:

@Autowired MyBean mbForEnterprise1; //In the classes of the first enterprise
@Autowired MyBean mbForEnterprise2; //In the classes of the second enterprise

还有这个:

MyBean mb = (MyBean)MyFactory.get("myBeanForEntreprise1"); //In the classes of the first enterprise
MyBean mb = (MyBean)MyFactory.get("myBeanForEntreprise2"); //In the classes of the second enterprise

无论如何,您都需要在应用程序中更改某些内容,无论是类还是配置文件,但您必须这样做并重新部署它。

如果有这样的方法,那不是很好吗:

MyBean mb = (MyBean)MyFactory.get("mb"); 

那样做,您可以设置工厂的代码以在运行时根据已登录用户企业获取正确的实现?? 现在那将非常有帮助。 您只需添加一个新的jar文件,其中包含新类,并设置规则,甚至可以在运行时进行设置(或者如果您留下此选项,则添加新的配置文件),无需更改现有类。 这将是一个动态工厂!

这难道不比为每个企业编写两个配置文件,甚至每个企业都有两个不同的应用程序更有帮助吗?

你可以告诉我,我永远不需要在运行时切换,所以我配置应用程序,如果我继承该类或使用其他实现,我只需更改配置并重新部署即可。 好吧,这也可以通过工厂完成。 老实说,你有多少次这样做? 也许只有当您有一个将在公司中的其他地方使用的应用程序时,您才会执行此操作,然后您将传递代码给另一个团队,他们会执行此类操作。 但是嘿,这也可以通过工厂完成,并且使用动态工厂会更好!!

无论如何,评论区开放供您谴责。


3
太多开发人员认为依赖注入框架是为了带来新的东西。正如你所解释的,传统工厂(通常是抽象工厂)可以扮演反转依赖的同样角色。反转与注入?对我来说,依赖注入框架唯一的“好处”就是当添加/更改依赖时,我们不需要改变/重新编译代码(通常可通过像Spring这样的XML进行配置)。为什么要避免重新编译?为了防止在更改依赖/工厂时出现潜在的人为错误。但是使用我们优秀的IDE,重构可很好地发挥作用 :) - Mik378

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