20

当我第一次开始看到反单身的评论时,我很困惑。我在最近的一些项目中使用了单例模式,并且效果很好。这么多,事实上,我已经用过很多次了。

现在,在遇到一些问题后,阅读了这个SO 问题,尤其是这篇博文,我明白了我给这个世界带来的邪恶。

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

例如:
在一个零售店管理程序中,我使用了 MVC 模式。我的模型对象描述了商店,用户界面是视图,我有一组控制器作为两者之间的联系。伟大的。除了我把 Store 变成了一个单例(因为应用程序一次只管理一个商店),我还把我的大部分 Controller 类变成了单例(一个 mainWindow、一个 menuBar、一个 productEditor...)。现在,我的大部分 Controller 类都可以像这样访问其他单例:

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

我应该改为:

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

Globals 仍然很糟糕,但至少他们不会假装

我看到#1 很快导致了可怕的构造函数调用:

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

还有其他人经历过吗?让许多单独的类访问公共变量而不是全局或单例的 OO 方法是什么?

4

8 回答 8

19

依赖注入是你的朋友。

看看优秀的谷歌测试博客上的这些帖子:

希望有人为 C++ 世界制作了一个 DI 框架/容器?看起来谷歌已经发布了一个C++ 测试框架和一个C++ 模拟框架,这可能会帮助你。

于 2009-01-23T21:30:38.293 回答
3

我避免单例的方法源于“应用程序全局”并不意味着“VM 全局”(即static)的想法。因此,我介绍了一个ApplicationContext包含许多以前的static单例信息的类,这些信息应该是应用程序全局的,例如配置存储。该上下文被传递到所有结构中。如果您使用任何 IOC 容器或服务管理器,则可以使用它来访问上下文。

于 2009-01-23T21:29:12.527 回答
3

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

于 2009-01-23T21:50:16.150 回答
3

在程序中使用全局或单例没有任何问题。不要让任何人对这种废话持教条主义的态度。规则和模式是很好的经验法则。但归根结底,这是您的项目,您应该对如何处理涉及全局数据的情况做出自己的判断。

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

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

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

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

模式只是经验法则。在您的汽车变道之前,您是否总是使用转向信号灯?如果你是普通人,你通常会遵守规则,但如果你在凌晨 4 点在空旷的高速公路上开车,谁在乎,对吧?有时它会咬你一口,但这是可控的风险。

于 2009-05-05T16:51:38.960 回答
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该类不应该关心自己的创建(并且巧合地遵循单一责任原则作为将工厂方法移出的效果)。

从这里你有很多选择,但我会把它留给你自己练习。这里很容易对模式进行过度设计(或过热)。我的建议是仅在需要时应用模式。

于 2009-01-23T21:36:42.447 回答
1

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

也就是说,在您的示例中,应用程序具有明显的自由度:有人可能会说“但我想要两个商店”。

有几种解决方案。首先出现的是建立一个工厂类;当您请求一个 Store 时,它​​会为您提供一个以某个通用名称(例如,URI)命名的 Store。在该 Store 中,您需要确保多个副本不会通过关键区域或某种方法相互叠加确保事务的原子性。

于 2009-01-23T21:32:30.880 回答
1

Miško Hevery有一篇关于可测试性的不错的文章系列,其中包括单例,他不仅在讨论问题,而且还讨论了如何解决它(请参阅“修复缺陷”)。

于 2009-01-23T21:33:51.020 回答
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);
  }
};

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

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

于 2009-01-23T23:04:44.767 回答