静态工厂vs单身工厂


24

在我的某些代码中,我有一个类似于以下内容的静态工厂:

public class SomeFactory {

    // Static class
    private SomeFactory() {...}

    public static Foo createFoo() {...}

    public static Foo createFooerFoo() {...}
}

在代码审查期间,建议将其作为一个单例并注入。因此,它应如下所示:

public class SomeFactory {

    public SomeFactory() {}

    public Foo createFoo() {...}

    public Foo createFooerFoo() {...}
}

需要强调的几件事:

  • 两个工厂都是无状态的。
  • 方法之间的唯一区别是它们的范围(实例与静态)。实现是相同的。
  • Foo是没有接口的bean。

我要静态化的参数是:

  • 该类是无状态的,因此不需要实例化
  • 能够调用静态方法比必须实例化工厂似乎更自然。

工厂作为单例的参数是:

  • 注入一切都很好
  • 尽管工厂是无状态的,但使用注入进行测试还是比较容易的(易于模拟)
  • 测试消费者时应该嘲笑它

我对单例方法有一些严重的问题,因为它似乎表明任何方法都不应该是静态的。它还似乎暗示StringUtils应该包装和注入诸如此类的实用程序,这似乎很愚蠢。最后,这意味着我需要在某个时候模拟工厂,这似乎不正确。我想不起来何时需要模拟工厂。

社区对此有何看法?尽管我不喜欢单例方法,但我似乎对此没有强烈的反对意见。


2
工厂正在使用的东西。要单独测试该东西,您需要为其提供工厂的模拟。如果您不嘲笑工厂,那么一个错误可能会导致单元测试失败,而如果不这样做的话。
Mike

因此,您是在提倡使用静态实用程序,还是只使用静态工厂?
bstempi

1
我总体上反对他们。例如,在C#中,出于完全相同的原因,很难对DateTimeand File类进行测试。例如,如果您有一个在构造函数中将Created日期设置为的类,那么DateTime.Now如何使用间隔5分钟创建的两个对象来创建单元测试?那相隔几年呢?您真的不能这样做(无需大量工作)。
Mike

2
您的单例工厂不应该有private构造函数和getInstance()方法吗?抱歉,挑剔的人!
TMN

1
@Mike-同意-我经常使用DateTimeProvider类,该类琐碎地返回DateTime.Now。然后,您可以在测试中将其交换为模拟,该模拟返回预定时间。这是额外的工作,把它传递给所有的构造-最头痛的是,当同事不买它100%,到DateTime.Now滑明确提到出于懒惰... :)
朱莉娅海沃德

Answers:


15

为什么要将工厂与它创建的对象类型分开?

public class Foo {

    // Private constructor
    private Foo(...) {...}

    public static Foo of(...) { return new Foo(...); }
}

这就是约书亚·布洛赫(Joshua Bloch)在其书“有效Java”第5页上描述为项目1的内容。Java非常详细,无需添加额外的类,单例和其他功能。在某些情况下,使用单独的工厂类是有意义的,但它们之间相距甚远。

为了解释约书亚·布洛赫(Joshua Bloch)的第1项,与构造函数不同,静态工厂方法可以:

  • 具有可以描述特定类型的对象创建的名称(Bloch使用probablePrime(int,int,Random)作为示例)
  • 返回一个现有的对象(想想:flyweight)
  • 返回原始类的子类型或它实现的接口
  • 减少冗长(指定类型参数-例如参见Bloch)

缺点:

  • 没有公共或受保护的构造函数的类不能被子类化(但是您可以使用受保护的构造函数和/或第16项:优先考虑继承而不是继承)
  • 静态工厂方法不会与其他静态方法脱颖而出(使用标准名称,如of()或valueOf())

确实,您应该将Bloch的书交给代码审阅者,看看你们两个是否同意Bloch的风格。


我也考虑过这一点,我想我喜欢它。我不记得为什么我首先将它们分开。
bstempi

一般来说,我们确实同意Bloch的风格。最终,我们将把工厂方法移到它们实例化的类中。我们的解决方案与您的解决方案之间的唯一区别是,构造函数将保持公开状态,以便人们仍可以直接实例化该类。感谢您的回答!
bstempi

但是...太可怕了。您很可能想要一个实现IFoo的类,并将其传递给它。您必须将IFoo硬编码为Foo才能获取实例。如果您具有包含IFoo create()的IFooFactory,则客户端代码不需要知道选择了哪个具体Foo作为IFoo的实现细节。
威尔伯特,2013年

@Wilbert有public static IFoo of(...) { return new Foo(...); } 什么问题?Bloch列表满足了您的关注,这是此设计的优点之一(上面的第二个要点)。我一定不能理解你的评论。
GlenPeterson

为了创建实例,此设计期望:Foo.of(...)。但是,永远不要暴露Foo的存在,而应该只知道IFoo(因为Foo只是IFoo 具体体现)。在具体的impl上使用静态工厂方法可以解决此问题,并导致客户端代码和具体实现之间的耦合。
威尔伯特,2013年

5

就在我脑海中,这是Java中的一些静态问题:

  • 静态方法不是OOP的“规则”所扮演的角色。就我所知,您不能强迫一个类实现特定的静态方法。因此,不能有多个类使用相同的签名实现相同的静态方法集。所以,你不得不拥有一个不管它是你正在做的。您也不能真正地继承静态类。因此没有多态性,等等。

  • 由于方法不是对象,因此静态方法无法传递,也无法使用(除非进行大量的样板代码编写),除非通过硬链接到它们的代码进行,这使客户端代码高度耦合的。(公平地说,这不仅是静态的错误)。

  • 静态字段与全局字段非常相似


您提到:

我对单例方法有一些严重的问题,因为它似乎表明任何方法都不应该是静态的。似乎还建议应该包装并注入诸如StringUtils之类的实用程序,这似乎很愚蠢。最后,这意味着我需要在某个时候模拟工厂,这似乎不正确。我想不起来何时需要模拟工厂。

这使我好奇地问你:

  • 您认为静态方法的优点是什么?
  • 您认为StringUtils设计和实施正确吗?
  • 您为什么认为您永远不需要嘲笑工厂?

嘿,谢谢你的回答!按照您要求的顺序:(1)静态方法的优点是语法糖和缺少不必要的实例。读取具有静态导入的代码并说出类似Foo f = createFoo();而不是拥有命名实例的代码,这是很好的。实例本身不提供任何实用程序。(2)我是这样认为的。它旨在克服此类方法中许多不存在的事实String。替换一些根本String无法解决的问题,因此使用utils是可行的方法。(续)
bstempi

它们易于使用,不需要您关心任何实例。他们感到自然。(3)因为我的工厂与内的工厂方法非常相似Integer。它们非常简单,逻辑很少,仅是为了提供方便(例如,没有其他面向对象的理由要使用它们)。我的论点的主要部分是它们不依赖状态-没有理由在测试期间将它们隔离。人们StringUtils在测试时不会嘲笑-他们为什么需要嘲笑这个简单的便利工厂?
bstempi

3
他们不会嘲笑,StringUtils因为可以合理地确保那里的方法没有任何副作用。
罗伯特·哈维,

@RobertHarvey我想我对我的工厂也有同样的主张-它没有任何修改。就像在StringUtils不更改输入的情况下返回一些结果一样,我的工厂也没有进行任何修改。
bstempi

@bstempi我在那里打算使用苏格拉底式的方法。我想我很烂。

0

我认为您正在构建的应用程序必须相当简单,否则您的生命周期就相对较早。我能想到的唯一唯一的情况是,如果您设法将了解工厂的代码的其他部分限制在一两个地方,那么您的静态变量就不会遇到问题。

想象一下您的应用程序在不断发展,现在在某些情况下您需要产生一个FooBar(Foo的子类)。现在,您需要遍历代码中所有与自己有关的地方,SomeFactory并编写某种逻辑来查看someCondition和调用SomeFactoryor SomeOtherFactory。实际上,看看您的示例代码,情况比这更糟。您打算只在创建不同的Foos的方法之间添加方法,并且所有客户端代码都必须在确定要创建哪个Foo之前​​检查条件。这意味着所有客户都需要对Factory的工作方式有深入的了解,并且每当需要(或删除!)新Foo时,他们都需要进行更改。

现在想象一下,如果您刚刚创建了一个IFooFactory,则会生成一个IFoo。现在,产生不同的子类甚至是完全不同的实现是孩子的事—您只需在链的更高层输入正确的IFooFactory,然后当客户端获取它时,它将调用gimmeAFoo()并将获得正确的foo,因为它获得了正确的Factory 。

您可能想知道这在生产代码中如何发挥作用。为了给您一个我正在开发的代码库中的示例,经过一年多的项目扩展后,该示例开始趋于平稳。我有很多工厂用来创建Question数据对象(它们类似于Presentation Models)。我有一个QuestionFactoryMap基本上是一个哈希,用于查找何时使用哪个问题工厂。因此,为了创建一个提出不同问题的应用程序,我在地图中放置了不同的工厂。

可以在“说明”或“练习”(都实现IContentBlock)中提出问题,因此每个问题InstructionsFactoryExerciseFactory都会获得QuestionFactoryMap的副本。然后,将各种说明和练习工厂移交给ContentBlockBuilder该再次维护何时使用的哈希。然后,当加载XML时,ContentBlockBuilder会从散列中调用Exercise或Instructions工厂,并要求将其提交给createContent(),然后Factory将问题的构建委托给它根据其内部QuestionFactoryMap提取的内容。在每个XML节点与散列中。不幸的是,那东西是BaseQuestion一个IQuestion,但这只是表明,即使您在设计时保持谨慎,也可能犯下会困扰您的错误。

您的肠胃告诉您这些静电会伤害您,并且文献中肯定有很多内容可以支持您的肠胃。您不会因为仅依赖于单元测试而使用依赖注入(这并不是我相信,如果您构建好的代码不会发现自己更多地使用它),您将永远不会迷失,但是静态会完全破坏您的能力快速轻松地修改您的代码。那为什么还要去那里呢?


考虑一下类中的工厂方法Integer-简单的事情,例如从String转换。就是Foo那样 我Foo不打算被扩展(我的错是因为没有说明这一点),所以我没有您的多态性问题。我了解拥有多态工厂的价值并在过去使用过它们...我只是认为它们不适合在这里使用。您是否可以想象是否必须实例化工厂来对Integer当前通过静态工厂进行的简单操作进行处理?
bstempi

另外,您对应用程序的假设还算不错。该应用程序相当复杂。这Foo,然而,比许多类的要简单得多。这只是一个简单的POJO。
bstempi

如果要对Foo进行扩展,那么就可以将Factory用作实例了-您提供了正确的Factory实例来为您提供正确的Subclass,而不是if (this) then {makeThisFoo()} else {makeThatFoo()}通过代码全部调用。我注意到关于长期建筑不良的人的一件事是,他们就像患有慢性疼痛的人一样。他们实际上并不知道没有痛苦的感觉,所以他们所遭受的痛苦似乎并不那么糟糕。
艾米·布兰肯希

另外,您可能想检查一下有关静态方法(甚至是数学方法)问题的链接
Amy Blankenship 2013年

我已经更新了问题。您和另一个人提到扩展Foo。没错,我从未宣布过最终决定。 Foo是不打算扩展的bean。
bstempi
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.