我应该如何重构我的代码以删除不必要的单身?

时间:2009-01-23 21:21:49

标签: language-agnostic refactoring singleton

当我第一次看到反单身评论时,我很困惑。我在最近的一些项目中使用了单例模式,并且它运行得很漂亮。事实上,我已经使用过很多次了。

现在,在遇到一些问题后,阅读this SO问题,特别是this博客文章,我理解了我带给世界的邪恶。

那么:我如何从现有代码中删除 单例?

例如:
在零售店管理程序中,我使用了MVC模式。我的Model对象描述了商店,用户界面是View,我有一组控制器,它们充当两者之间的联络。大。除了我将Store变成一个单独的(因为应用程序一次只管理一个商店),我还把我的大部分Controller类变成了单例(一个mainWindow,一个menuBar,一个productEditor ......)。现在,我的大多数Controller类都可以访问其他单例:

Store managedStore = Store::getInstance();
managedStore.doSomething();
managedStore.doSomethingElse();
//etc.

我应该改为:

  1. 创建每个对象的一个​​实例,并将引用传递给需要访问它们的每个对象?
  2. 使用全局变量?
  3. 还有别的吗?

Globals仍然很糟糕,但至少它们不会是pretending

我看到#1迅速导致构造函数调用非常膨胀:

someVar = SomeControllerClass(managedStore, menuBar, editor, sasquatch, ...)

还有其他人经历过这个吗?如果不是全局变量或单个变量,那么为多个单独的类访问公共变量的OO方法是什么?

8 个答案:

答案 0 :(得分:19)

Dependency Injection是你的朋友。

查看excellent Google Testing Blog上的这些帖子:

希望有人为C ++世界制作了一个DI框架/容器?看起来Google发布了C++ Testing FrameworkC++ Mocking Framework,这可能对您有帮助。

答案 1 :(得分:3)

我避免单身人士的方法源于“应用程序全局”并不意味着“虚拟机全球化”(即static)的观点。因此,我引入了一个ApplicationContext类,它包含许多应用程序全局的前static单例信息,就像配置存储一样。此上下文将传递到所有结构中。如果您使用任何IOC容器或服务管理器,则可以使用它来访问上下文。

答案 2 :(得分:3)

这不是单身人士的问题。有一个只有一个实例的对象是没关系的。问题是全球访问。您使用Store的类应该在构造函数中接收Store实例(或者具有可以设置的Store属性/数据成员),并且它们都可以接收相同的实例。 Store甚至可以在其中保留逻辑,以确保只创建一个实例。

答案 3 :(得分:3)

在程序中使用全局或单例没有任何问题。不要让任何人因为那种废话而欺骗你。规则和模式是很好的经验法则。但最终这是你的项目,你应该自己判断如何处理涉及全球数据的情况。

无限制地使用全局变量是个坏消息。但只要你勤奋,他们就不会杀了你的项目。系统中的某些对象应该是单例。标准输入和输出。你的日志系统。在游戏中,您的图形,声音和输入子系统,以及游戏实体的数据库。在GUI中,您的窗口和主要面板组件。您的配置数据,插件管理器,Web服务器数据。对于您的应用程序而言,所有这些内容或多或少都具有全局性。我认为你的Store类也会传递它。

很清楚使用全局变量的成本是多少。您的应用程序的任何部分都可能正在修改它。当每行代码都是调查中的嫌疑人时,追踪错误很难。

但是不使用全局变量的成本呢?就像编程中的其他一切一样,这是一种权衡。如果避免使用全局变量,则最终必须将这些有状态对象作为函数参数传递。或者,您可以将它们传递给构造函数并将它们保存为成员变量。当你有多个这样的对象时,情况就会恶化。您现在线程您的州。在某些情况下,这不是问题。如果您知道只有两个或三个函数需要处理该有状态Store对象,那么这是更好的解决方案。

但在实践中,情况并非总是如此。如果您的应用程序的每个部分都触及您的商店,您将把它线程化为十几个功能。最重要的是,其中一些功能可能具有复杂的业务逻辑。当您使用辅助函数打破业务逻辑时,您必须 - 更多地处理您的状态!比如说你意识到深度嵌套的函数需要来自Store对象的一些配置数据。突然,您必须编辑3或4个函数声明以包含该store参数。然后你必须返回并将商店添加为实际参数,以调用其中一个函数。可能是函数对Store的唯一用途是将它传递给需要它的某个子函数。

模式只是经验法则。你是否始终使用你的转向信号才能改变你的车道?如果你是普通人,你通常会遵循这个规则,但如果你是在凌晨4点开空行驶的,那么谁会给你一个废话,对吗?有时它会咬你的屁股,但这是一个有管理的风险。

答案 4 :(得分:2)

关于膨胀的构造函数调用问题,您可以引入参数类或工厂方法来为您利用此问题。

参数类将一些参数数据移动到它自己的类,例如像这样:

var parameterClass1 = new MenuParameter(menuBar, editor);
var parameterClass2 = new StuffParameters(sasquatch, ...);

var ctrl = new MyControllerClass(managedStore, parameterClass1, parameterClass2);

它只是将问题转移到其他地方。您可能希望改为维护构造函数。只保留构造/启动相关类时重要的参数,并使用getter / setter方法(或者如果你正在使用.NET)执行其余操作。

工厂方法是一种创建类所需的所有实例的方法,并且具有封装所述对象的创建的好处。它们也非常容易从Singleton重构,因为它们与您在Singleton模式中看到的getInstance方法类似。假设我们有以下非线程安全的简单单例示例:

// The Rather Unfortunate Singleton Class
public class SingletonStore {
    private static SingletonStore _singleton
        = new MyUnfortunateSingleton();

    private SingletonStore() {
        // Do some privatised constructing in here...
    }

    public static SingletonStore getInstance() {
        return _singleton;
    }  

    // Some methods and stuff to be down here
}

// Usage: 
// var singleInstanceOfStore = SingletonStore.getInstance();

很容易将其重构为工厂方法。解决方案是删除静态引用:

public class StoreWithFactory {

    public StoreWithFactory() {
        // If the constructor is private or public doesn't matter
        // unless you do TDD, in which you need to have a public 
        // constructor to create the object so you can test it.
    }

    // The method returning an instance of Singleton is now a
    // factory method. 
    public static StoreWithFactory getInstance() {
        return new StoreWithFactory(); 
    }
}

// Usage:
// var myStore = StoreWithFactory.getInstance();

用法仍然相同,但你不会因为拥有一个实例而陷入困境。当然,您可以将此工厂方法移动到它自己的类中,因为Store类不应该关注自身的创建(并且巧合地遵循单一责任原则作为移动工厂方法的效果)。

从这里你有很多选择,但我会把它作为锻炼给自己。这里很难对模式进行过度设计(或过热)。我的提示是仅在有need for it时才应用模式。

答案 5 :(得分:1)

好吧,首先,“单身人士总是邪恶的”概念是错误的。只要有资源不会或不能复制,就可以使用Singleton。没问题。

那就是说,在你的例子中,应用程序中有一个明显的自由度:有人可以过来说“但我想要两个商店。”

有几种解决方案。首先出现的是建造一个工厂阶级;当你要求一个商店时,它会给你一个带有一些通用名称的名字(例如,一个URI)。在那个商店里面,你需要确保多个副本不会通过关键区域或某些方法相互踩踏。确保交易的原子性。

答案 6 :(得分:1)

Miško Hevery有一篇关于可测试性的精彩系列文章,其中包括singleton,他不仅讨论问题,还讨论如何解决问题(参见'修复漏洞') “)。

答案 7 :(得分:1)

我喜欢鼓励在必要时使用单身人士,同时不鼓励使用单身人士模式。注意单词的区别。单例(小写)用于只需要一个实例的地方。它在程序开始时创建,并传递给需要它的类的构造函数。

class Log
{
  void logmessage(...)
  { // do some stuff
  }
};

int main()
{
  Log log;

  // do some more stuff
}

class Database
{
  Log &_log;
  Database(Log &log) : _log(log) {}
  void Open(...)
  {
    _log.logmessage(whatever);
  }
};

使用单例提供了Singleton反模式的所有功能,但它使您的代码更容易扩展,并且使其可测试(在Google测试博客中定义的单词意义上)。例如,我们可能决定在某些时候也需要能够登录到Web服务,使用我们可以轻松完成的单例,而无需对代码进行重大更改。

相比之下,Singleton模式是全局变量的另一个名称。它从未在生产代码中使用过。