为什么要使用工厂类而不是直接对象构造?


162

我已经在GitHub和CodePlex上看到了几个С#和Java类库项目的历史,并且看到了切换到工厂类而不是直接对象实例化的趋势。

为什么要广泛使用工厂类?我有一个很好的库,通过老式的方式创建对象-通过调用类的公共构造函数。在最后的提交中,作者迅速将所有数千个类的公共构造函数更改为内部构造,并且还创建了一个具有数千个CreateXXX静态方法的巨大工厂类,这些静态方法通过调用类的内部构造函数来返回新对象。外部项目API损坏了,做得很好。

为什么这样的更改有用?用这种方式重构的目的是什么?用静态工厂方法调用替换对公共类构造函数的调用有什么好处?

什么时候应该使用公共构造函数,什么时候应该使用工厂?



1
什么是面料类/方法?
2014年

Answers:


56

工厂类之所以被实施是因为它们使项目更紧密地遵循SOLID原则。特别是,接口隔离和依赖关系反转的原理。

工厂和接口提供了更多的长期灵活性。它允许更解耦的设计,因此更易于测试。这是为什么您会走这条路的详尽列表:

  • 它使您可以轻松引入控制反转(IoC)容器
  • 可以模拟接口,这使您的代码更具可测试性
  • 更改应用程序时,它为您提供了更大的灵活性(即,您可以创建新的实现而无需更改相关代码)

考虑这种情况。

程序集A(->取决于):

Class A -> Class B
Class A -> Class C
Class B -> Class D

我想将B类移到依赖于组件A的程序集B上。有了这些具体的依存关系,我必须在整个类层次结构中移动大部分。如果使用接口,则可以避免很多麻烦。

大会A:

Class A -> Interface IB
Class A -> Interface IC
Class B -> Interface IB
Class C -> Interface IC
Class B -> Interface ID
Class D -> Interface ID

现在,我可以毫不费力地将B类转移到B组。它仍然取决于程序集A中的接口。

使用IoC容器来解决您的依赖关系,可以为您带来更大的灵活性。更改类的依赖关系时,无需更新对构造函数的每次调用。

遵循接口隔离原理和依赖关系反转原理,我们可以构建高度灵活,分离的应用程序。一旦您使用了其中一种类型的应用程序,您将再也不想回到使用new关键字的状态了。


40
IMO工厂对于SOLID来说是最不重要的。您可以在没有工厂的情况下进行SOLID。
欣快2014年

26
对我来说从来没有意义的一件事是,如果您使用工厂制造新对象,则必须首先制造工厂。那到底能给您带来什么呢?是否假设别人会给您工厂,而不是您自己实例化工厂,还是其他?答案中应该提到这一点,否则不清楚工厂如何实际解决任何问题。
Mehrdad 2014年

8
@BЈовић-除了每次添加新的实现外,现在您都可以打开工厂并对其进行修改以说明新的实现-明显违反了OCP。
Telastyn

6
@Telastyn是的,但是使用创建的对象的代码不会更改。这比工厂变更更重要。
2014年

8
工厂在答案所针对的某些领域很有用。它们不适合在任何地方使用。例如,使用工厂创建字符串或列表将花费太多时间。即使对于UDT,也不总是必需的。关键是在需要取消接口的确切实现时使用工厂。

126

就像whatsisname所说的那样,我相信这是货物崇拜软件设计的案例。工厂,尤其是抽象工厂,仅在您的模块创建一个类的多个实例并且您想使该模块的用户能够指定要创建的类型时才可用。实际上,这种要求很少见,因为在大多数情况下,您只需要一个实例,并且可以直接传递该实例,而无需创建显式工厂。

事实是,工厂(和单例)非常容易实现,因此人们即使在不必要的地方也经常使用它们。因此,当程序员认为“我应该在此代码中使用哪些设计模式?”时,首先想到的是工厂。

创建许多工厂的原因是“记住,也许有一天,我将需要以不同的方式创建这些类”。这明显违反了YAGNI

当您引入IoC框架时,工厂就会过时,因为IoC只是一种工厂。许多IoC框架都可以创建特定工厂的实现。

同样,没有设计模式可以使用CreateXXX仅调用构造函数的方法创建巨大的静态类。它尤其不被称为工厂(也不叫抽象工厂)。


6
我同意您的大多数观点,但“ IoC是工厂”:IoC容器不是工厂。它们是实现依赖项注入的便捷方法。是的,有些可以建造自动工厂,但是它们本身不是工厂,因此不应被视为如此。我也主张YAGNI观点。能够在您的单元测试中替换测试双倍是很有用的。在事实发生之后重构一切以提供此功能是一个痛苦的过程。提前计划,不要为“ YAGNI”辩解
AlexFoxGill 2014年

11
@AlexG-嗯...实际上,几乎所有的IoC容器都是作为工厂工作的。
Telastyn

8
@AlexG IoC的主要重点是基于配置/约定构造要传递给其他对象的具体对象。这与使用工厂相同。而且您不需要工厂就可以创建用于测试的模拟。您只需实例化并直接传递模拟。就像我在第一段中所说的那样。只有在您想要将实例的创建传递给模块的用户(称为工厂)时,工厂才有用。
欣快2014年

5
有一个区别。该工厂模式是使用由消费者一个程序运行过程中创建的实体。一个IoC容器用于创建在启动期间的程序的对象图。您可以手动执行此操作,容器只是一种便利。工厂的消费者不应意识到IoC容器。
AlexFoxGill 2014年

5
关于:“而且您不需要工厂就可以创建用于测试的模拟。您只需实例化并直接传递该模拟即可” –同样,这是不同的情况。您使用工厂来请求实例 -使用者可以控制此交互。通过构造函数或方法提供实例无效,这是另一种情况。
AlexFoxGill 2014年

79

工厂模式的流行源于“ C风格”语言(C / C ++,C#,Java)的编码人员中几乎信条的信念,即使用“ new”关键字是不好的,应不惜一切代价避免使用(或最不集中)。反过来,这来自对单一责任原则(SOLID的“ S”)以及对依赖倒置原则(“ D”)的超严格解释。简而言之,SRP表示理想情况下,一个代码对象应具有一个“更改理由”,并且只有一个;“更改原因”是该对象的主要目的,它在代码库中的“职责”,以及其他需要更改代码的内容,都不需要打开该类文件。DIP甚至更简单。一个代码对象绝不应该依赖于另一个具体对象,

举例来说,通过使用“ new”和公共构造函数,您可以将调用代码耦合到特定具体类的特定构造方法。您的代码现在必须知道类MyFooObject存在,并且具有接受字符串和int的构造函数。如果该构造函数需要更多信息,则必须更新该构造函数的所有用法以传递该信息,包括您现在正在编写的信息,因此要求它们具有有效的传递对象,因此,它们必须具有或对其进行更改以获取它(向使用对象添加更多责任)。此外,如果在代码库中曾经用BetterFooObject替换MyFooObject,则必须更改旧类的所有用法以构造新对象,而不是旧对象。

因此,相反,MyFooObject的所有使用者都应直接依赖于“ IFooObject”,它定义了实现包括MyFooObject在内的类的行为。现在,IFooObject的使用者不能只构造IFooObject(无需知道特定的具体类是IFooObject,而他们不需要),因此必须给他们一个IFooObject实现类或方法的实例。从外部,由另一个对象负责,该对象负责知道如何针对这种情况创建正确的IFooObject,在我们看来,这通常称为Factory。

现在,这里是理论与现实相遇的地方;一个对象永远不可能一直对所有类型的更改都关闭。举例来说,IFooObject现在是代码库中的另一个代码对象,只要使用者或IFooObjects的实现所需的接口发生更改,该对象就必须更改。这就引入了新的复杂度,涉及跨此抽象更改对象彼此交互的方式。此外,如果接口本身被新的接口所取代,则消费者仍将不得不做出更深刻的改变。

优秀的编码人员知道如何通过分析设计并找到很有可能需要以特定方式进行更改的地方,并重构它们以使其更能容忍YAGNI(“您将不需要它”)与SOLID之间取得平衡。这种类型的变化,因为这样的话:“你会需要它。”


21
喜欢这个答案,特别是在阅读完所有其他答案之后。我可以补充一下,几乎所有(好的)新编码员都对原则过于教条,因为他们很想成为一个好人,但还没有学会使事情保持简单且不至于过大的价值。
jean

1
公共构造函数的另一个问题是,没有一种好的方法让类Foo指定公共构造函数应可用于创建Foo实例,或在同一包/程序集中创建其他类型,而不应用于创建从其他地方派生的类型。我不知道有什么特别令人信服的理由,即语言/框架无法定义用于new表达式的单独构造函数,而不是子类型构造函数的调用,但是我不知道任何能做到这一点的语言。
超级猫

1
受保护的和/或内部的构造函数将是这样的信号;该构造函数仅可用于消费代码,无论是在子类中还是在同一程序集中。C#没有用于“保护的和内部的”的关键字组合,这意味着只有程序集中的子类型可以使用它,但是MSIL具有该可见性的作用域标识符,因此可以想象C#规范可以扩展以提供一种利用它的方式。但是,这实际上与工厂的使用没有太大关系(除非您使用可见性限制来强制工厂的使用)。
KeithS 2015年

2
完美的答案。就在“理论与现实相遇”部分。试想一下,成千上万的开发人员和工作人员花了很多时间来赞美,称赞,实施,使用它们,然后再跌至您所描述的相同状态,这简直令人发疯。继YAGNI,我从来没有确定需要实现一个工厂
布雷诺萨尔加多

我在一个代码库上编程,因为OOP实现不允许重载构造函数,所以有效地禁止使用公共构造函数。在允许的语言中,这已经是一个小问题。喜欢创造温度。华氏温度和摄氏温度都是浮子。但是,您可以将它们装箱,然后解决问题。
jgmjgm '18年

33

当构造函数包含简短的简单代码时,它们很好。

当初始化不仅仅是为字段分配一些变量时,工厂就有意义了。这里有一些好处:

  • 在专用的类(工厂)中,冗长而复杂的代码更有意义。如果将相同的代码放入调用一堆静态方法的构造函数中,则将污染主类。

  • 在某些语言和某些情况下,在构造函数中引发异常是一个非常糟糕的主意,因为它可能会引入错误。

  • 调用构造函数时,调用者需要知道要创建的实例的确切类型。并非总是这种情况(作为Feeder,我只需要构造Animal即可提供它;我不在乎它是a Dog还是a Cat)。


2
选择不仅是“工厂”或“构造函数”。A Feeder可能不会使用它们,而是调用它的Kennel对象的getHungryAnimal方法。
DougM 2014年

4
+1我发现在构造函数中遵守绝对没有逻辑的规则并不是一个坏主意。构造函数只能用于通过将其参数值分配给实例变量来设置对象的初始状态。如果需要更复杂的方法,则至少要使用工厂(类)方法来构建实例。
KaptajnKold 2014年

这是我在这里看到的唯一令人满意的答案,这使我不必编写自己的答案。其他答案仅涉及抽象概念。
TheCatWhisperer

但是这个论点也可以成立Builder Pattern。是不是
soufrk

建设者统治
Breno Salgado

10

如果使用接口,则可以独立于实际实现。可以配置工厂(通过属性,参数或其他方法)以实例化许多不同实现中的一种。

一个简单的例子:您想与设备通信,但不知道它是通过以太网,COM还是USB。您定义一个接口和3个实现。然后,在运行时,您可以选择所需的方法,然后工厂将为您提供适当的实现。

经常使用...


5
我要补充一点,当接口有多种实现调用代码不知道或不应该选择哪个时,使用它非常有用。但是,当工厂方法只是针对单个构造函数的静态包装时就是反模式。有必须是多个实现从中挑或工厂的方式获得和增加不必要的复杂性。

现在,您有了更多的灵活性,并且从理论上讲,您的应用程序可以使用以太网,COM,USB和串行接口,并且可以进行Fireloop或其他任何操作。实际上,您的应用程序只能通过以太网进行通信。
Pieter B'2

7

这是Java / C#的模块系统中存在限制的征兆。

原则上,没有理由不应该将具有相同构造函数和方法签名的类的一种实现换成另一种。还有语言,让这一点。但是,Java和C#坚持每个类都有唯一的标识符(全限定名),并且客户端代码最终以硬编码依赖于此。

你可以排序的解决这个问题用文件系统和编译器选项,以便顺藤摸瓜com.example.Foo映射到不同的文件,但是这是令人惊讶的和直观。即使这样做,您的代码仍然只与该类的一种实现联系在一起。也就是说,如果编写Foo依赖于类的类MySet,则可以MySet在编译时选择的实现,但仍不能Foo使用的两个不同实现来实例化MySet

这个不幸的设计决定迫使人们interface不必要地使用s来使代码过时,以防止他们以后需要某种事物的不同实现或促进单元测试的可能性。这并不总是可行的。如果您有任何方法可以查看该类的两个实例的私有字段,则将无法在接口中实现它们。例如,这就是为什么您没有union在Java Set界面中看到的原因。尽管如此,在数字类型和集合之外,二进制方法并不常见,因此您通常可以摆脱它。

当然,如果调用,new Foo(...)您仍然对class有依赖性,因此,如果您希望类能够直接实例化接口,则需要工厂。但是,通常最好的方法是在构造函数中接受该实例,然后让其他人决定使用哪个实现。

由您决定是否值得使用接口和工厂来膨胀代码库。一方面,如果所讨论的类在您的代码库内部,则重构代码以使其将来使用不同的类或接口是微不足道的;否则,请执行以下操作。您可以在发生这种情况时调用YAGNI并稍后进行重构。但是,如果该类是您已发布的库的公共API的一部分,则无法选择修复客户端代码。如果您不使用interface,以后又需要多个实现,那么您将陷入困境。


2
我希望Java和类似.NET的派生类对于创建其自身实例的类具有特殊的语法,否则new希望成为调用特殊命名的静态方法的语法糖(如果类型具有“公共构造函数”,则该方法将自动生成) ”,但未明确包含该方法)。恕我直言,如果代码只想要一个无聊的默认实现List,那么接口应该有可能在不需要客户端知道任何特定实现的情况下(例如ArrayList)为它提供一个。
2014年

2

在我看来,他们只是使用简单工厂,这不是适当的设计模式,不应与抽象工厂或工厂方法相混淆。

而且,由于他们创建了“带有数千个CreateXXX静态方法的巨大结构类”,因此听起来像是反模式(也许是God类?)。

我认为简单工厂和静态创建者方法(不需要外部类)在某些情况下会很有用。例如,当对象的构造需要各种步骤时,例如实例化其他对象(例如,有利于合成)。

我什至不称其为Factory,而只是封装在带有后缀“ Factory”的某个随机类中的一堆方法。


1
简单工厂有它的位置。想象一个实体有两个构造函数参数,int xIFooService fooService。您不想fooService到处走动,所以您可以使用方法创建工厂Create(int x)并将服务注入工厂内部。
AlexFoxGill 2014年

4
@AlexG然后,您必须IFactory到处传递而不是IFooService
欣快2014年

3
我同意欣快;根据我的经验,从顶部插入到图中的对象往往是所有低层对象都需要的类型,因此传递IFooServices没什么大不了的。用另一种替换一个抽象不会完成任何事情,只会进一步混淆代码库。
KeithS 2014年

这似乎与所问的问题完全无关:“为什么要使用工厂类而不是直接的对象构造?何时应该使用公共构造函数,何时应该使用工厂?” 请参阅如何回答
蚊蚋

1
那只是问题的标题,我想您错过了其余的问题。请参阅提供的链接的最后一点;)。
FranMowinckel '16

1

作为库的用户,如果库具有工厂方法,则应使用它们。您将假定工厂方法使库的作者可以灵活地进行某些更改而不会影响您的代码。例如,他们可能会在工厂方法中返回某个子类的实例,而该方法不适用于简单的构造函数。

作为库的创建者,如果您想自己使用这种灵活性,则可以使用工厂方法。

在您描述的情况下,似乎给人的印象是用工厂方法替换构造函数是没有意义的。对于参与其中的每个人来说无疑是痛苦的;没有充分的理由,库就不应从其API中删除任何内容。因此,如果我添加了工厂方法,那么我将使现有的构造函数可用(也许不推荐使用),直到工厂方法不再只是调用该构造函数并且使用普通构造函数的代码效果不如预期为止。您的印象很可能是正确的。


另请注意;如果开发人员需要为派生类提供额外的功能(额外的属性,初始化),则开发人员可以执行此操作。(假设工厂方法是可重写的)。API作者还可以提供针对特殊客户需求的解决方案。
埃里克·施耐德

-4

在Scala和函数式编程时代,这似乎已经过时了。坚实的功能基础取代了庞大的类。

还要注意,{{使用工厂时,Java的double 不再起作用,即

someFunction(new someObject() {{
    setSomeParam(...);
    etc..
}})

可以让您创建一个匿名类并对其进行自定义。

在时空困境中,由于CPU的快速性,时间因素现在缩水了很多,功能编程可以缩小空间,即代码大小现在很实用。


2
这看起来更像是切线评论(请参阅“ 如何回答”),并且似乎并没有提供对先前9个答案中提出和解释的要点的实质内容
gnat
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.