单例模式的替代方案


27

我对单例模式有不同的看法。一些人主张应不惜一切代价避免使用它,而另一些人则认为在某些情况下它可能是有用的。

我使用单例的一种情况是当我需要一个工厂(假设类型F的对象f)来创建某个类A的对象时。工厂使用一些配置参数创建一次,然后每次使用类型A被实例化。因此,要实例化A的代码的每个部分都获取单例f并创建新实例,例如

F& f                   = F::instance();
boost::shared_ptr<A> a = f.createA();

所以我的一般情况是

  1. 出于优化原因(不需要多个工厂对象)或共享公共状态(例如,工厂知道仍可以创建A的实例),我只需要一个类的实例即可
  2. 我需要一种方法可以在代码的不同位置访问F的此实例f。

我对讨论此模式的好坏不感兴趣,但是假设我想避免使用单例,我还可以使用其他什么模式?

我的想法是(1)从注册表获取工厂对象,或(2)在程序启动期间的某个时候创建​​工厂,然后将工厂作为参数传递。

在解决方案(1)中,注册表本身是一个单例,因此我刚刚将不使用单例的问题从工厂转移到了注册表。

在情况(2)中,我需要工厂对象来自的一些初始源(对象),因此恐怕我会再次落入另一个单例(提供我的工厂实例的对象)。通过跟踪此单身人士链,我可以将问题简化为一个单身人士(整个应用程序),通过它 可以直接或间接管理所有其他单身人士

最后一种选择(使用一个初始单例创建所有其他唯一对象并将所有其他单例注入正确的位置)是否可以接受?当有人建议不要使用单例时,这是隐式建议的解决方案,还是其他解决方案,例如在上述示例中?

编辑

由于我认为我的问题的重点已经被某些人误解了,因此这里提供了更多信息。如所解释的,例如这里,词语 可指示(a)中的一类具有单个实例对象和(b)用于创建和访问这样的对象的设计模式。

为了使事情更清楚,让我们对(a)使用术语唯一对象, 对(b)使用术语单例模式。因此,我知道单例模式和依赖项注入是什么(顺便说一句,最近我一直在大量使用DI从我正在处理的某些代码中删除单例模式的实例)。

我的观点是,除非整个对象图都是从位于main方法堆栈上的单个对象实例化的,否则始终需要通过singleton模式访问一些唯一的对象。

我的问题是,完整的对象图创建和连接是否取决于主要方法(例如,通过一些不使用模式本身的强大DI框架)是唯一的无 单例模式解决方案。


8
依赖注入...
猎鹰

1
@Falcon:依赖注入...是什么?DI并不能解决该问题,尽管它通常可以为您提供隐藏它的便捷方法。
pdr 2012年

@Falcon您的意思是IoC容器吗?这将使您能够在一行中解决依赖关系。例如Dependency.Resolve <IFoo>(); 或Dependency.Resolve <IFoo>()。DoSomething();
CodeART 2012年

这是一个很老的问题,并且已经回答了。我还在虽然这将是很好的这一个链接:stackoverflow.com/questions/162042/...
TheSilverBullet

Answers:


7

第二种选择是一个很好的方法-这是一种依赖注入,这是当您要避免单例和全局变量时用于在程序之间共享状态的模式。

你不能回避这样一个事实的东西必须创建你的工厂。如果那恰好是应用程序,那就去吧。重要的一点是,您的工厂不必理会创建它的对象,接收工厂的对象也不应该依赖工厂的来源。不要让您的对象获得指向应用程序单例的指针并向工厂索要它。让您的应用程序创建工厂并将其提供给需要它的对象。


17

单例的目的是强制某个域中只能存在一个实例。这意味着,如果您有充分的理由强制执行单例行为,则单例很有用。实际上,这种情况很少见,多处理和多线程肯定会模糊“唯一”的含义-它是每台计算机,每个进程,每个线程,每个请求一个实例吗?您的单例实现会照顾竞争条件吗?

除了单例,我更喜欢使用以下任一方法:

  • 短暂的本地实例,例如对于工厂而言:典型的工厂类具有最小的状态(如果有的话),并且没有真正的理由在达到其目的后使它们保持活动状态;99%的实际场景中创建和删除类的开销都无需担心
  • 传递一个实例,例如对于一个资源管理器:它们必须是长期的,因为加载资源非常昂贵,并且您希望将它们保留在内存中,但是绝对没有理由阻止创建更多的实例-谁知道,也许几个月后再有第二个资源经理会很有意义...

原因是单例是变相的全局状态,这意味着它在整个应用程序中引入了高度的耦合-您代码的任何部分都可以以最小的努力从任何地方获取单例实例。如果您使用本地对象或传递实例,则您将拥有更多的控制权,并且可以缩小范围,缩小依赖范围。


3
这是一个很好的答案,但我并不是真正在问为什么/不应该使用单例模式。我知道人与全球国家可能会遇到的麻烦。无论如何,问题的主题是如何拥有唯一的对象以及如何实现单例模式的免费实现。
乔治

3

大多数人(包括您)完全误解了Singleton模式的实际含义。Singleton模式仅意味着存在一个类的实例,并且在整个应用程序中都有某种机制可以使代码获得对该实例的引用。

在GoF书中,返回对静态字段的引用的静态工厂方法只是该机制的外观示例,并且存在严重缺陷。不幸的是,每个人和他们的狗都陷入了这种机制,并认为这就是Singleton的全部目的。

实际上,您引用的替代方案也是Singletons,只是具有获取参考的不同机制。(2)显然会导致过多的传递,除非您仅在调用堆栈根附近的几个地方需要引用。(1)听起来像是一个粗糙的依赖注入框架-那么为什么不使用一个呢?

优点是现有的DI框架灵活,功能强大且经过了良好的测试。他们可以做的不仅仅是管理Singletons。但是,要充分利用它们的功能,如果您以某种方式构造应用程序(这并不总是可能的话),它们将发挥最佳作用:理想情况下,有一些中心对象是通过DI框架获取的,并且其所有依赖项都被传递并被填充然后执行。

编辑: 最终,无论如何,一切都取决于主要方法。但是您是对的:完全避免使用global / static的唯一方法是通过main方法进行所有设置。请注意,DI在主要方法不透明并设置服务器的服务器环境中最为流行,而应用程序代码基本上由由服务器代码实例化和调用的回调组成。但是大多数DI框架还允许针对“特殊情况”直接访问其中央注册表,例如Spring的ApplicationContext。

因此,到目前为止,基本上人们能想到的最好的事情就是您提到的两种选择的巧妙组合。


您是说基本概念依赖项注入基本上是我上面概述的第一种方法(例如,使用注册表),最后是基本概念?
乔治

@Giorgio:DI总是包括这样的注册表,但是使它变得更好的要点是,正如我在上面所写的那样,您不必在需要对象的地方查询注册表,而是拥有可传递地填充的中心对象。
Michael Borgwardt'5

@Giorgio:通过一个具体示例更容易理解:假设您有一个Java EE应用程序。您的前端逻辑位于JSF托管Bean的操作方法中。当用户按下按钮时,JSF容器将创建Managed Bean的实例并调用action方法。但是在此之前,DI组件会查看Managed Bean类的字段,并且具有一定批注的字段将按照DI配置填充对Service对象的引用,并且那些Service objec也会发生相同的情况。然后,该操作方法可以调用服务。
Michael Borgwardt'5

有些人(包括您)没有仔细阅读问题,并且误解了实际询问的内容。
乔治

1
@Giorgio:您最初的问题中没有任何内容表明您已经熟悉依赖注入。并查看更新的答案。
Michael Borgwardt'5

3

有几种选择:

依赖注入

每个对象在创建时都会将其依赖项传递给它。通常,框架或少量工厂类负责创建和连接对象

服务注册

每个对象都传递一个服务注册表对象。它提供了请求提供不同服务的各种不同对象的方法。Android框架使用此模式

根管理员

有一个对象是对象图的根。通过跟随该对象的链接,最终可以找到其他任何对象。代码看起来像这样:

GetSomeManager()->GetRootManager()->GetAnotherManager()->DoActualThingICareAbout()

除了模拟之外,DI与使用单例相比还有哪些优势?这个问题困扰了我很长时间。至于注册表和根管理器,不是将它们实现为单例还是通过DI实现?(实际上,IoC容器一个注册表,对吧?)
Konrad Rudolph

1
@KonradRudolph,具有DI的依赖关系是显式的,并且该对象不假设如何提供给定资源。对于单例,依赖项是隐式的,每次使用都假定只有一个这样的对象。
温斯顿·埃韦特

@KonradRudolph,是否使用Singletons或DI实现其他方法?是和否。最后,您必须传递对象或以全局状态存储对象。但是,您在执行此操作方面存在细微的差异。上面提到的三个版本都避免使用全局状态。但是最终代码获取引用的方式不同。
温斯顿·埃韦特

如果单例实现了一个接口(甚至没有实现),并且您将单例实例传递给方法,而不是Singleton::instance在客户端代码中的任何地方查询,则使用单例不会假设有单个对象。
康拉德·鲁道夫2012年

@KonradRudolph,那么您实际上正在使用某些混合的Singleton / DI方法。我的纯粹主义者认为您应该采用纯粹的DI方法。但是,单例的大多数问题并没有真正适用于此。
温斯顿·埃韦特

1

如果您可以将单例的需求简化为一个函数,为什么不使用简单的工厂函数呢?全局函数(在您的示例中可能是类F的静态方法)本质上是单例,由编译器和链接器强制执行唯一性。

class Factory
{
public:
    static Object* createObject(...);
};

Object* obj = Factory::createObject(...);

诚然,当单例的操作不能简化为单个函数调用时,这种方法会崩溃,尽管一小部分相关函数可能会让您满意。

话虽如此,问题中的第1项和第2项清楚地表明,您确实只是想要某种东西。根据您对单例模式的定义,您是否已经在使用它或非常接近它。我认为如果没有一个单身人士或至少非常接近一个人,您就无法拥有其中的一个。它太接近单例的含义。

正如您所建议的,在某个时候您必须拥有某种东西,所以问题可能不在于某个东西的单一实例,而是采取措施来防止(或至少阻止或最小化)滥用它。将尽可能多的状态移出“单个”并移入参数是一个好的开始。缩小与单身人士的接口也有帮助,因为以这种方式滥用的机会较少。有时,您只需要吸收它并使单例变得非常健壮,例如堆或文件系统。


4
我个人认为这只是单例模式的不同实现。在这种情况下,单例实例就是类本身。特别是:它具有与“真实”单例相同的缺点。
约阿希姆·绍尔

@Randall Cook:我认为您的回答抓住了我的观点。我经常读到,单例非常非常糟糕,应该不惜一切代价避免。我对此表示同意,但另一方面,我认为从技术上永远避免它是不可能的。当您需要“某物”时,您必须在某处创建它并以某种方式访问​​它。
乔治

1

如果您使用的是C ++或Python这样的多范式语言,单例类的一种替代方法是包装在名称空间中的一组函数/变量。

从概念上讲,带有自由全局变量,自由全局函数和用于信息隐藏的静态变量的C ++文件都包装在名称空间中,几乎为您带来与单例“类”相同的效果。

如果要继承,它只会崩溃。我已经看到很多单身人士本来可以改善这种情况。


0

单例封装

案例方案。您的应用程序是面向对象的,即使您有更多的类,也需要3或4个特定的单例。

在示例之前(C ++伪代码):

// network info
class Session {
  // ...
};

// current user connection info
class CurrentUser {
  // ...
};

// configuration upon user
class UserConfiguration {
  // ...
};

// configuration upon P.C.,
// example: monitor resolution
class TerminalConfiguration {
  // ...
};

一种方法是通过将所有(单个)单例封装为一个唯一的单例(将其他单例作为成员)来部分删除某些(全局)单例。

这样,我们将“将所有单例封装成一个方法”,而不是“几个单例方法与一个方法,根本没有一个单例方法”。

在Example(C ++伪代码)之后:

// network info
class NetworkInfoClass {
  // ...
};

// current user connection info
class CurrentUserClass {
  // ...
};

// configuration upon user
// favorite options menues
class UserConfigurationClass {
  // ...
};

// configuration upon P.C.,
// example: monitor resolution
class TerminalConfigurationClass {
  // ...
};

// this class is instantiated as a singleton
class SessionClass {
  public: NetworkInfoClass NetworkInfo;
  public: CurrentUserClass CurrentUser;
  public: UserConfigurationClass UserConfiguration;
  public: TerminalConfigurationClass TerminalConfiguration;

  public: static SessionClass Instance();
};

请注意,这些示例更像是伪代码,并且忽略了较小的错误或语法错误,并考虑了该问题的解决方案。

还有其他事情要考虑,因为所使用的编程语言可能会影响如何实现单例或非单例实现。

干杯。


0

您的原始要求:

所以我的一般情况是

  1. 出于优化原因(不需要多个工厂对象)或共享公共状态,我只需要一个类的实例(例如,工厂知道它仍可以创建多少个> A实例)
  2. 我需要一种方法可以在代码的不同位置访问F的此实例f。

不要与单例的定义(以及稍后要引用的内容)保持一致。从GoF(我的版本是1995)第127页:

确保一个类只有一个实例,并提供对其的全局访问点。

如果您只需要一个实例,那么这并不妨碍您制作更多实例。

如果您想要一个可全局访问的实例,请制作一个可全局访问的实例。无需为您所做的每件事命名一个模式。是的,单个可全局访问的实例通常是不好的。给他们一个名字并不会减少他们的坏处

为了避免全局可访问性,常见的“创建实例并将其传递”或“让两个对象的所有者将它们粘合在一起”的方法效果很好。


0

如何使用IoC容器?经过一番思考,您可以得出以下结论:

Dependency.Resolve<IFoo>(); 

您必须指定IFoo在启动时使用哪种实现,但这是一次性配置,以后可以轻松替换。通常,可以通过IoC容器来控制已解析实例的生存期。

静态单例void方法可以替换为:

Dependency.Resolve<IFoo>().DoSomething();

静态单例getter方法可以替换为:

var result = Dependency.Resolve<IFoo>().GetFoo();

示例与.NET有关,但是我敢肯定,在其他语言中也可以实现类似的效果。

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.