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

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

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

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

评论

describe.joelonsoftware.com/default.asp?joel.3.219431.12&

什么是面料类/方法?

在meta
上讨论了这个问题
@PieterB现在是2020,您2014的链接已死。您知道该讨论的副本吗?

@EvgeniNabokov danstroot.com/2018/10/03/hammer-factories关于锤子工厂的故事

#1 楼

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

功能和接口可以提供更多的长期灵活性。它允许更解耦的设计,因此更易于测试。这是为什么您会走这条路的非详尽清单:


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

请考虑这种情况。

组件A(->取决于):

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


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

组装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关键字了。

评论


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

– up
14年8月14日在6:29

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

–user541686
2014年8月14日上午10:14

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

– Telastyn
14年8月14日在12:58

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

–BЈовић
14年8月14日在13:34

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

–user22815
2014年8月14日14:43

#2 楼

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

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

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

引入IoC框架时,工厂变得过时了,因为IoC只是一种工厂。而且许多IoC框架都可以创建特定工厂的实现。

此外,也没有设计模式可以使用CreateXXX方法创建大型静态类,而这些静态类仅调用构造函数。而且它尤其不被称为工厂(也不叫抽象工厂)。

评论


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

– AlexFoxGill
2014年8月14日11:29



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

– Telastyn
14年8月14日在12:59

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

– up
2014年8月14日13:41



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

– AlexFoxGill
2014年8月14日14:16



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

– AlexFoxGill
2014年8月14日14:26



#3 楼

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

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

因此,相反,MyFooObject的所有使用者都应直接依赖于“ IFooObject”,它定义了实现包括MyFooObject在内的类的行为。现在,IFooObject的使用者不能仅构造IFooObject(无需知道特定的具体类是IFooObject,而他们不需要),因此必须为他们提供IFooObject实现类或方法的实例。

现在,这是理论与现实相遇的地方;从外部,由另一个对象负责了解如何为这种情况创建正确的IFooObject。在我们看来,该对象通常称为工厂。一个对象永远不会一直对所有类型的更改都关闭。举例来说,IFooObject现在是代码库中的另一个代码对象,只要使用者或IFooObjects的实现所需的接口发生更改,该对象就必须更改。这就引入了新的复杂度,涉及跨此抽象更改对象彼此交互的方式。此外,如果接口本身被新的接口取代,消费者仍将不得不做出更深层次的改变。

一个好的编码员知道如何平衡YAGNI(“您将不需要它“)使用SOLID,可以通过分析设计并找到很有可能需要以特定方式进行更改的地点,并对其进行重构以使其更能容忍这种类型的更改,因为在这种情况下,“您将需要它”。

评论


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

–让
14年8月14日在17:48

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

–超级猫
2015年2月11日在16:12



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

– KeithS
2015年5月4日15:52



完美的答案。就在“理论与现实相遇”部分。试想一下,成千上万的开发人员和工作人员花了很多时间来赞美,称赞,实施,使用它们,然后再跌入您所描述的相同状态,这简直令人发疯。在YAGNI之后,Ive从未发现实施工厂的必要性

–布雷诺·萨尔加多(Breno Salgado)
17年12月9日,下午1:54

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

– jgmjgm
18年2月11日在18:25

#4 楼

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

当初始化不仅仅是为字段分配几个变量时,工厂就有意义了。这里有一些好处:在专用类(工厂)中,长而复杂的代码更有意义。如果将相同的代码放入调用大量静态方法的构造函数中,则会污染主类。
在某些语言和某些情况下,在构造函数中引发异常是一个非常糟糕的主意,因为它可能会引入错误。
调用构造函数时,调用者需要知道要创建的实例的确切类型。并非总是这种情况(作为Feeder,我只需要构造Animal即可提供它;我不在乎它是Dog还是Cat)。


评论


选择不仅是“工厂”或“构造函数”。 Feeder可能不使用它们,而是将其称为Kennel对象的getHungryAnimal方法。

–DougM
14年8月14日在13:13

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

– KaptajnKold
14年8月14日在15:07

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

–TheCatWhisperer
17年3月17日在18:15

但是,对于Builder模式,该参数也可以成立。是不是

– soufrk
17年9月26日在14:07

构造规则

–布雷诺·萨尔加多(Breno Salgado)
17年12月9日,下午1:58

#5 楼

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

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

经常使用它...

评论


我要补充一点,当接口有多种实现且调用代码不知道或不应该选择哪个时,使用它非常有用。但是,当工厂方法只是针对单个构造函数的静态包装时,就是反模式。必须有多种实现方式可供选择,否则工厂将妨碍您的工作并增加不必要的复杂性。

–user22815
2014年8月14日14:46



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

– Pieter B
16-2-1在12:11



#6 楼

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

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

您可以通过以下方式来解决此问题摆弄文件系统和编译器选项,以便com.example.Foo映射到其他文件,但这是令人惊讶且不直观的。即使这样做,您的代码仍然只与该类的一种实现联系在一起。即如果编写依赖于类Foo的类MySet,则可以在编译时选择MySet的实现,但仍无法使用Foo的两种不同实现来实例化MySet

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

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

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

评论


我希望Java和.NET等派生类对创建其自身实例的类具有特殊的语法,否则希望将新的语法简单地用作调用特殊命名的静态方法(如果类型具有“ public”则将自动生成)构造函数”,但未明确包含该方法)。恕我直言,如果代码只是想要实现List的无聊的默认事物,那么接口应该可以为它提供一个,而客户端不必知道任何特定的实现(例如ArrayList)。

–超级猫
14年8月14日在20:39

#7 楼

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

由于他们创建了“巨大的带有数千个CreateXXX静态方法的fabric类”,这听起来像是一种反模式(也许是上帝类?)。

我认为Simple Factory和static creator方法(不需要外部方法)类),在某些情况下会很有用。例如,当对象的构造需要各种步骤时,例如实例化其他对象(例如,有利于合成)。

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

评论


简单工厂有它的位置。假设一个实体接受两个构造函数参数,即int x和IFooService fooService。您不想到处都传递fooService,因此您可以使用Create(int x)方法创建工厂并将服务注入工厂内部。

– AlexFoxGill
14年8月14日在14:29

@AlexG然后,您必须在各处传递IFactory而不是IFooService。

– up
2014年8月14日15:10

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

– KeithS
2014年8月14日在16:10



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

– gna
16 Mar 7 '16 at 21:07

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

– FranMowinckel
16年3月7日在21:56

#8 楼

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

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

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

评论


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

–埃里克·施耐德(Eric Sc​​hneider)
18-10-31在16:26

#9 楼

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

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

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


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

由于快速的CPU,功能性编程可以缩小空间,即代码大小,因此在时间空间难题中,时间因数已经大大缩小了。现在很实用。

评论


这看起来更像是切线评论(请参阅“如何回答”),并且似乎并没有提供对先前9个答案中所提出和解释的要点的实质性建议。

– gna
16 Mar 7 '16 at 21:05