“单身”工厂,好还是坏?

Posted

技术标签:

【中文标题】“单身”工厂,好还是坏?【英文标题】:"Singleton" factories, ok or bad? 【发布时间】:2010-11-20 02:01:09 【问题描述】:

我有很多(抽象)工厂,它们通常被实现为单例。

通常是为了方便,不必将它们通过与使用或了解这些工厂无关的层。

大多数时候我只需要在启动时决定其余代码程序的工厂实现,也许通过一些配置

它看起来例如喜欢

abstract class ColumnCalculationFactory  
  private static ColumnCalculationFactory factory;

 public static void SetFactory(ColumnCalculationFactory f) 
         factory = f;
  

  public static void Factory() 
         return factory;
  

 public IPercentCalculation CreatePercentCalculation();
 public IAverageCalculation CreateAverageCalculation();
    ....


有些东西确实闻到了这个味道,我只是不确定是什么 - 它可能更像是一个disuised global而不是一个单身人士。并不是真的必须只有一个工厂创建 ColumnCalculations - 尽管我的程序不需要更多。

这被认为是最佳做法吗?我是否应该将这些东西放在一些(半)全局 AppContext 类中?别的东西(我还没准备好切换到一些更大的 IoC 容器,或者 spring.net 顺便说一句)?

【问题讨论】:

【参考方案1】:

这实际上取决于您在做什么以及您的应用程序的范围。如果它只是一个相当小的应用程序并且永远不会超出此范围,那么您当前的方法可能很好。这些事情没有普遍的“最佳”实践。虽然我不建议将单例用于无状态叶方法和/或单向调用(例如日志记录)之外的任何东西,但“仅仅因为”它是一个单例并不一定是正确的做法。

除了琐碎或原型代码之外,我个人更喜欢通过构造函数注入显式使用控制反转,因为这意味着所有依赖项都被考虑在内,您不会得到任何“惊喜”。编译器不会让你在没有 B 的情况下实例化 A 和在没有 C 的情况下实例化 B。单例会立即隐藏这些关系——你可以在没有 B 的情况下实例化 A 和在没有 C 的情况下实例化 B 。当从 A 到 B 的调用发生时,你会得到一个空引用例外。

这在测试时特别烦人,因为您必须通过运行时故障反复向后工作。当您测试代码时,您使用的 API 就像其他程序员一样,因此它表明了这种方法的设计问题。构造函数注入确保这种情况永远不会发生——所有的依赖关系都是预先说明的。构造函数注入的缺点是对象图的配置更复杂。这可以通过使用 IoC 容器来缓解。

我想我想说的是,如果您已经到了考虑使用某种上下文对象和注册表模式的地步,那么您不妨看看 IoC 容器。当您可以使用像 Autofac 这样成熟的免费产品时,努力推出自己的 mutt 版本可能是浪费时间。

【讨论】:

对于控制反转,它们是我可以使用的不包括 Spring 和 XML 的模式吗?我不知道有什么方法可以使用 IC 来“预加载”没有 Spring 的构造函数,因此是一个 Web 项目。非网络项目呢? @Chrips IoC 实际上只需要我们不创建类之间的直接依赖关系。相反,我们为类提供了所需对象的接口。这使我们能够轻松地模拟测试事物,或使代码适应我们需要的任何其他用例。【参考方案2】:

有几个单例是很典型的,通常不会有问题——很多单例会导致一些烦人的代码。

我们刚刚经历了一种情况,我们不得不测试我们大量单例负载的类。问题是当你在测试 b 类时,它得到了 c 类(单例),你无法模拟出 c 类(至少 EasyMock 不允许我们替换单例类的静态工厂方法。

一个简单的解决方法是为所有单例设置“Setters”以进行测试。不太推荐。

我们尝试的另一件事是创建一个包含所有单例的类——一个注册表。这样做非常接近依赖注入,这几乎肯定是你应该使用的。

除了测试之外,我很久以前就了解到,给定对象的实例永远不会超过一个;在下一个版本中,他们通常需要两个,这使得单例比静态类好得多——至少你可以在单例的 getter 中添加一个参数并返回第二个而不需要太多重构(这再次只是做 DI 所做的事情)。

无论如何,看看 DI,你可能真的很开心。

【讨论】:

同意。单例的另一个后果是在单元测试期间“泄漏”设置值。我们不得不为每个测试派生 JVM,因为被测单例或仅被测试使用的单例处于状态 A,而下游它需要处于状态 B。 扩展 TDD 的问题在于,您经常为了使用测试工具而使代码复杂化并使其更容易出错。我坚信如果你必须为了 Mocking 而改变架构,你就是在过度使用 Mocking,而应该使用 Factory/Singleton 的假设配置。 100% 的代码覆盖率是疯狂的,并且会导致比它所能解决的更多的问题和复杂性。 我同意 TDD,但这不是使用 DI 的唯一原因。此外,无论测试的实际存在如何,TESTABLE 代码通常都会比不可测试代码更好。即使没有 100% 的测试覆盖率,使用 DI 而不是单例也不是什么坏事,而且在您实际可能想要测试它的情况下会非常好。【参考方案3】:

不,因为您在这里所做的是创建全局状态。全局状态存在各种各样的问题——其中主要是一个函数以相当不可见的方式依赖于其他函数的行为。如果一个函数调用另一个函数在完成之前忘记存储和恢复factory,那么您就会遇到问题,因为除非您将旧值存储在某个地方,否则您甚至无法取回旧值。而且您必须添加代码才能做到这一点(从您的代码来看,我猜您使用的是finally 的语言,这为错误留下了更大的空间)。更重要的是,如果你最终得到的代码需要在两个工厂之间为两个子对象快速切换,你必须在每个点编写一个方法调用——你不能在每个子对象中存储状态(好吧,你可以,但是你打败了全球状态的目的[诚然不多])。

将工厂类型存储为成员并将其传递给需要它的新对象的构造函数(或根据需要创建一个新对象等)可能是最有意义的。它还为您提供了更好的控制 - 您可以保证对象 A 构造的所有对象都经过同一个工厂,或者您可以提供交换工厂的方法。

【讨论】:

虽然暴露的公共 SetFactory 不能保证有人不能更换工厂的更多混乱,但我通常似乎只在启动时设置工厂,基于初始决定或配置。跨度> 那么你就会成为单身人士的缺点……他们是单身人士。【参考方案4】:

这被认为是最佳做法吗?

我认为没问题:您说“我的程序不需要更多”,因此您实现更灵活/抽象的东西可能是 You Ain't Gonna Need It 的情况。

【讨论】:

【参考方案5】:

我建议不要这样做,因为它很难为调用这些工厂的代码编写体面的单元测试。

Misko Hevery 在他的博客上对此有一个 nice article。

【讨论】:

当然可以,但是在一个不知道类依赖关系的非平凡应用程序中,您是否会立即知道要模拟哪些?同样,我参考了 Misko 的文章。 Misko 在 youtube 上也有一些关于这个主题的优秀视频,youtube.com/watch?v=acjvKJiOvXw 谢谢! Misko的文章很棒,cmets值得一读! 也值得一读:misko.hevery.com/2008/10/21/…【参考方案6】:

单例的不良形式是实现与您的工厂方法等效的形式:

return new CalculationFactory ();

这更难存根、模拟或包装。

返回在别处创建的对象的版本要好得多,尽管与大多数东西一样,可能会被误用或过度使用。

【讨论】:

以上是关于“单身”工厂,好还是坏?的主要内容,如果未能解决你的问题,请参考以下文章

工厂模式--摆脱你日复一日new对象却依旧单身的苦恼!

工厂模式--摆脱你日复一日new对象却依旧单身的苦恼!

2017 acm / icpc shenyang 双十一单身狗温馨重现赛

单身人士:好的设计还是拐杖? [关闭]

图书馆类的单身人士

HDU 4507 吉哥系列故事――恨7不成妻(数位dp&好魔性的一道好题)