it-swarm.cn

那么Singletons不好,那又如何呢?

最近,关于使用(和过度使用)Singleton的问题进行了很多讨论。我也是我职业生涯早期的那些人之一。我可以看到问题出在哪里,但是,在很多情况下,我看不到尼斯的替代方案-而且,很少有关于反辛格尔顿的讨论真正提供了解决方案。

这是我参与的一个近期重大项目的真实示例:

该应用程序是一个胖客户端,具有许多单独的屏幕和组件,它们使用来自服务器状态的大量数据,这些数据并不经常更新。这些数据基本上被缓存在一个Singleton“管理器”对象中-可怕的“全局状态”。想法是在应用程序中拥有一个位置,以保持数据的存储和同步,然后打开的任何新屏幕都可以从那里查询它们的大部分需求,而无需从服务器重复请求各种支持数据。不断地向服务器请求会占用太多带宽-我说的是每周要多付数千美元的互联网账单,所以这是不能接受的。

除了基本具有这种全局数据管理器缓存对象以外,还有其他合适的方法吗?当然,该对象不一定要正式是“ Singleton”,但从概念上讲,成为一个对象确实有意义。这里有什么不错的清洁选择?

570
Bobby Tables

在此处区分单个实例Singleton设计模式 非常重要。

单个实例只是一个现实。大多数应用仅设计为一次使用一种配置,一次使用一个UI,一次使用一个文件系统,等等。如果要维护很多状态或数据,那么您肯定会只想拥有一个实例,并使其保持尽可能长的生命周期。

单例设计模式是一种非常具体的单实例类型,特别是:

  • 可通过全局静态实例字段访问;
  • 在程序初始化或首次访问时创建;
  • 没有公共构造函数(无法直接实例化);
  • 从不明确释放(在程序终止时隐式释放)。

正是由于这种特定的设计选择,该模式引入了一些潜在的长期问题:

  • 无法使用抽象或接口类;
  • 无法分类;
  • 整个应用程序之间的高耦合(难以修改);
  • 难以测试(在单元测试中不能伪造/模拟);
  • 在可变状态下很难并行化(需要广泛的锁定);
  • 等等。

这些症状实际上都不是单个实例的地方病,只有Singleton模式。

您能做什么呢?只是不要使用Singleton模式。

引用问题:

想法是在应用程序中拥有一个位置,以保持数据的存储和同步,然后打开的任何新屏幕都可以从那里查询它们的大部分需求,而无需从服务器重复请求各种支持数据。不断地向服务器请求会占用太多带宽-我说的是每周要多付数千美元的互联网账单,所以这是不能接受的。

正如您所暗示的那样,这个概念有个名字,但听起来不确定。这就是 cache 。如果想花哨的话,可以将其称为“离线缓存”,也可以仅称为远程数据的脱机副本。

高速缓存不必是单例。如果要避免为多个缓存实例获取相同的数据,则可能必须是一个实例。但这并不意味着您实际上必须将所有内容暴露给所有人

我要做的第一件事是将缓存的不同功能区分离到单独的接口中。例如,假设您基于Microsoft Access制作了世界上最差的YouTube克隆:

 MSAccessCache 
▲
 | 
 + ----------------- + -------- --------- + 
 | | | 
 IMediaCache IProfileCache IPageCache 
 | | | 
 | | | 
 VideoPage MyAccountPage MostPopularPage 

在这里,您有几个界面描述了特定类可能需要访问的 specific 数据类型-媒体,用户个人资料和静态页面(如首页)。所有这些都是通过一个大型缓存实现的,但是您可以设计自己的类来代替接受接口,因此它们不在乎它们具有哪种实例。您可以在程序启动时初始化一次物理实例,然后才开始通过构造函数和公共属性传递实例(广播到特定的接口类型)。

顺便说一下,这叫做 Dependency Injection ;您不需要使用Spring或任何特殊的IoC容器,只要您的通用类设计接受调用者的依赖,而不是实例化它们它自己的引用全局状态

为什么要使用基于接口的设计?三个原因:

  1. 它使代码更易于阅读;您可以从接口中清楚地了解数据依赖类所依赖的对象。

  2. 如果并且当您意识到Microsoft Access不是数据后端的最佳选择时,您可以用更好的东西代替它-假设是SQL Server。

  3. 如果并且当您意识到SQL Server不是特定于媒体 /的最佳选择时,则可以中断实现,而不会影响系统的任何其他部分。那才是真正的抽象力量出现的地方。

如果要更进一步,则可以使用IoC容器(DI框架),例如Spring(Java)或Unity(.NET)。几乎每个DI框架都将执行其自己的生命周期管理,并且特别允许您将特定服务定义为单个实例(通常称为“单个”),但是那只是为了熟悉)。基本上,这些框架为您省去了手动传递实例的大部分繁琐工作,但并非绝对必要。 您不需要任何特殊工具即可实现此设计。

为了完整起见,我应该指出,以上设计确实也不理想。在处理高速缓存时(实际上),实际上应该有一个完全独立的 layer 。换句话说,这样的设计:

 +-IMediaRepository 
 | 
缓存(通用)--------------- +-IProfileRepository 
▲| 
 | +-IPageRepository 
 + ----------------- + ----------------- + 
 | | | 
 IMediaCache IProfileCache IPageCache 
 | | | 
 | | | 
 VideoPage MyAccountPage MostPopularPage 

这样做的好处是,如果您决定重构,甚至不需要分解Cache实例;您只需向媒体提供IMediaRepository的替代实现即可更改媒体的存储方式。如果考虑如何将它们组合在一起,您会发现它仍然只创建一个缓存的物理实例,因此您无需两次提取相同的数据。

这并不是说,世界上的每一个软件都必须按照这些严格的高内聚和松散耦合标准进行构建。它取决于项目的大小和范围,您的团队,您的预算,截止日期等。但是,如果您要问最佳设计是什么(代替单例使用),就可以了。

附言正如其他人所述,让依赖类知道它们正在使用 cache 可能不是最好的主意-这是他们根本不关心的实现细节。话虽这么说,总体架构仍与上图非常相似,只是您不会将单个接口称为 Caches 。相反,您可以将它们命名为 Services 或类似名称。

826
Aaronaught

在您给出的情况下,听起来好像不是使用Singleton,而是问题的征兆-一个更大的体系结构问题。

屏幕为什么要查询缓存对象中的数据?缓存对客户端应该是透明的。应该提供一个适当的抽象来提供数据,并且该抽象的实现可能会使用缓存。

问题可能是系统各部分之间的依存关系设置不正确,这可能是系统性的。

为什么屏幕需要知道它们从何处获取数据?为什么屏幕没有提供可以满足其数据请求的对象(在其后面隐藏了缓存)?通常,创建屏幕的职责不是集中的,因此注入依赖项没有明确的意义。

同样,我们正在研究大规模的建筑和设计问题。

同样,了解对象的生命周期可以与如何使用该对象完全脱离是很重要的,这是非常

缓存必须在应用程序的整个生命周期中都存在(才有用),因此对象的生命周期是Singleton。

但是Singleton(至少是Singleton作为静态类/属性的常见实现)的问题在于,其他使用它的类如何找到它。

对于静态Singleton实现,约定是在需要的地方简单地使用它。但这完全隐藏了依赖关系,并将两个类紧密结合在一起。

如果我们provide对类的依赖关系,则该依赖关系是显式的,并且所有使用类都需要了解的是可使用的合同。

48
quentin-starin

我就这个问题写了一个 整章 。大多数情况下是在游戏方面,但大多数应在游戏外部应用。

tl; dr:

四个单身帮的模式有两件事:使您可以从任何地方方便地访问对象,并确保只能创建该对象的一个​​实例。在99%的时间里,您所关心的只是前半部分,而在下半部分进行购物以增加不必要的限制。

不仅如此,还有更好的解决方案可以提供方便的访问。使对象成为全局对象是解决该问题的核选项,并使其易于破坏封装。不利于全局变量的一切都完全适用于单例。

如果您只是因为在代码中有很多地方需要使用同一对象而使用它,请尝试寻找一种更好的方法将其赋予just这些对象,而不要将其暴露给整个对象代码库。其他解决方案:

  • 完全抛弃它。我见过很多单例类,它们没有任何状态,只是一堆辅助函数。那些根本不需要实例。只需使它们成为静态函数,或将它们移入该函数作为参数的类之一即可。如果只需要123.Abs(),就不需要特殊的Math类。

  • 传递它。如果方法需要一些其他对象,则简单的解决方案是将其传递。传递一些对象没有错。

  • 将其放入基类中。如果您有很多都需要访问某些特殊对象的类,并且它们共享一个基类,则可以使该对象成为基础上的成员。构造它时,传入对象。现在派生的对象都可以在需要时获取它。如果对其进行保护,则确保该对象仍保持封装状态。

45
munificent

问题本身不是全球性的。

真的,您只需要担心global mutable state。恒定状态不受副作用影响,因此问题不大。

单例的主要问题是它增加了耦合,从而使诸如测试的工作变得更加艰巨。您可以通过从其他来源(例如工厂)获取单例来减少耦合。这将使您能够将代码与特定实例解耦(尽管您与工厂的耦合程度更高(但至少工厂可以为不同阶段提供替代实现))。

在您的情况下,只要您的单例实际上实现了一个接口(这样就可以在其他情况下使用替代方法),您就可以摆脱它。

但是单例的另一个主要缺点是,一旦就位,就将它们从代码中删除,并用其他东西替换就成为了一项艰巨的任务(再次存在耦合)。

// Example from 5 minutes (con't be too critical)
class ServerFactory
{
    public:
        // By default return a RealServer
        ServerInterface& getServer();

        // Set a non default server:
        void setServer(ServerInterface& server);
};

class ServerInterface { /* define Interface */ };

class RealServer: public ServerInterface {}; // This is a singleton (potentially)

class TestServer: public ServerInterface {}; // This need not be.
21
Martin York

那呢由于没有人说:工具箱。那就是如果您要全局变量

通过从另一个角度看问题,可以避免单身虐待。假设一个应用程序只需要一个类的一个实例,并且该应用程序在启动时就配置了该类:为什么类本身应该负责成为一个单例呢?由于应用程序需要这种行为,因此应用程序承担此责任似乎是很合逻辑的。应用程序而不是组件应该是单例。然后,应用程序使组件实例可供任何特定于应用程序的代码使用。当应用程序使用多个这样的组件时,它可以将它们聚合到我们所谓的工具箱中。

简而言之,应用程序的工具箱是一个单例,负责配置自身或允许应用程序的启动机制对其进行配置...

public class Toolbox {
     private static Toolbox _instance; 

     public static Toolbox Instance {
         get {
             if (_instance == null) {
                 _instance = new Toolbox(); 
             }
             return _instance; 
         }
     }

     protected Toolbox() {
         Initialize(); 
     }

     protected void Initialize() {
         // Your code here
     }

     private MyComponent _myComponent; 

     public MyComponent MyComponent() {
         get {
             return _myComponent(); 
         }
     }
     ... 

     // Optional: standard extension allowing
     // runtime registration of global objects. 
     private Map components; 

     public Object GetComponent (String componentName) {
         return components.Get(componentName); 
     }

     public void RegisterComponent(String componentName, Object component) 
     {
         components.Put(componentName, component); 
     }

     public void DeregisterComponent(String componentName) {
         components.Remove(componentName); 
     }

}

但猜猜怎么了?这是一个单身人士!

什么是单身人士?

也许这就是混乱的开始。

对我而言,singleton是一个强制为仅且始终具有单个实例的对象。您可以随时随地访问它,而无需实例化它。这就是为什么它与 static 如此密切相关的原因。为了进行比较,static基本相同,只是它不是实例。我们不需要实例化它,甚至不需要,因为它是自动分配的。这确实会带来问题。

根据我的经验,简单地将static替换为Singleton可以解决我正在进行的中型拼布袋项目中的许多问题。这仅意味着它确实可用于不良设计的项目。我认为 讨论 是否 单人模式有用 太多了-我真的不能争论它是否确实 不好 。但是仍然有 通常,支持单例而不是静态方法的好参数

我唯一确定的缺点是单例,而当我们在忽略良好做法的情况下使用它们时。这确实不是那么容易处理。但是,不良做法可以应用于任何模式。而且,我知道这太笼统了……我的意思是说有太多东西了。

不要误会我的意思!

简而言之,就像global vars一样, singletons应该仍然始终避免 。特别是因为他们被过度虐待。但是不能总是避免使用全局变量,在最后一种情况下我们应该使用它们。

无论如何,除了工具箱外,还有许多其他建议,就像工具箱一样,每个建议都有其应用程序...

其他选择

  • 我刚刚读过的有关单例的最佳文章 建议以 Service Locator 作为替代。对我来说,如果可以的话,基本上是“静态工具箱”。换句话说,使服务定位器为Singleton,您将拥有一个Toolbox。当然,这确实与避免单例的最初建议背道而驰,但这只是为了强制单例的问题是如何使用单例,而不是模式本身。

  • 其他 建议 工厂模式 作为替代。这是我从同事那里听到的第一个替代方法,我们很快就将其删除为global var。它肯定有其用法,但单例也是如此。

以上两种选择都是不错的选择。但这全取决于您的用法。

现在,暗示不惜一切代价避免单身人士是错误的…….

  • Aaronaught的答案建议 从不使用单例 ,出于一系列原因。但是,所有这些都是反对其使用和滥用方式的原因,而不是直接针对模式本身的原因。我完全同意所有那些担心的问题,我怎么能呢?我只是认为这具有误导性。

确实存在(抽象或子类的)无能,但是那又如何呢?这不是为了那个。就 我知道 而言,没有无法连接。高耦合也可以在那里,但这仅仅是因为它通常被使用。 不一定要 。实际上,耦合本身与单例模式无关。经过澄清,它也消除了测试的困难。至于并行化的难度,这取决于语言和平台,因此,模式也不是问题。

实际例子

我经常看到使用2,无论是赞成还是反对单身人士。Web缓存(我的情况)和日志服务

日志记录 有些人会争论 是一个完美的单例示例,因为我引用:

  • 请求者需要一个众所周知的对象,向其发送请求以进行日志记录。这意味着全球访问点。
  • 由于日志记录服务是单个事件源,多个侦听器可以注册到该事件源,因此只需要一个实例。
  • 尽管不同的应用程序可能登录到不同的输出设备,但是它们注册侦听器的方式始终相同。所有定制都通过侦听器完成。客户可以在不知道如何或在何处记录文本的情况下请求记录。因此,每个应用程序将完全相同地使用日志记录服务。
  • 任何应用程序都只能使用一个日志记录服务实例。
  • 任何对象都可以是日志记录请求者,包括可重用组件,因此它们不应与任何特定应用程序耦合。

尽管其他人会争辩说,一旦您最终意识到它实际上不应该只是一个实例,那么很难扩展日志服务。

好吧,我说这两个论点都是有效的。同样,这里的问题不在单例模式上。重构是否可行,取决于架构决策和权重。通常,重构是最后需要的纠正措施,这是另一个问题。

20
cregox

我的单例设计模式的主要问题是很难为您的应用程序编写好的单元测试。

与该“管理器”相关的每个组件都通过查询其单例实例来实现。而且,如果您要为此类组件编写单元测试,则必须将数据注入此单例实例,这可能并不容易。

另一方面,如果您的“经理”通过构造函数参数注入到从属组件中,而该组件不知道管理器的具体类型,则仅知道管理器实现的接口或抽象基类,则不知道该单元测试依赖项时,测试可以提供管理器的替代实现。

如果您使用IOC容器来配置和实例化组成应用程序的组件,则可以轻松地配置IOC容器以仅创建一个实例。 “管理器”,使您可以实现相同的控制全局应用程序缓存的实例。

但是,如果您不关心单元测试,那么单例设计模式就可以了。 (但我还是不会这样做)

5
Pete

就任何设计计算的好坏而言,单例并不是从根本上讲。它只能永远是正确的(给出预期的结果)或不正确。如果它使代码更清晰或更有效,它也可能有用或无效。

单例有用的一种情况是当它们表示一个真正独特的实体时。在大多数环境中,数据库是唯一的,实际上只有一个数据库。连接到该数据库可能很复杂,因为它需要特殊权限或遍历几种连接类型。仅出于这个原因,将连接组织为单例可能很有意义。

但是,您还需要确保单例确实是单例,而不是全局变量。当单个唯一数据库实际上是4个数据库,每个数据库分别用于生产,登台,开发和测试装置时,这一点就很重要。 Database Singleton将找出应连接的数据库实例,获取该数据库的单个实例,如果需要,则将其连接,然后将其返回给调用方。

当一个单例不是一个真正的单例时(大多数程序员不高兴),这是一个延迟实例化的全局变量,没有机会注入正确的实例。

设计良好的单例模式的另一个有用功能是它通常是不可观察的。呼叫者要求连接。提供它的服务可以返回一个池对象,或者如果它正在执行测试,则可以为每个调用者创建一个新对象,或者提供一个模拟对象。

4

代表实际对象的单例模式的使用是完全可以接受的。我为iPhone写作,并且Cocoa Touch框架中有很多单例。应用程序本身由UIApplication类的单例表示。您只有一个应用程序,因此以单例形式表示它是适当的。

只要设计得当,就可以将单例用作数据管理器类。如果这是一堆数据属性,那没有比全局范围更好。如果是一组getter和setter,那会更好,但仍然不是很好。如果这是一个真正管理数据所有接口的类,包括获取远程数据,缓存,设置和拆卸……那可能非常有用。

3
Dan Ray

单例只是面向服务的体系结构到程序中的投影。

API是协议级别的单例的示例。您可以通过本质上是单例的方式访问Twitter,Google等。那么,为什么单例在程序中变得不好呢?

这取决于您对程序的看法。如果您将程序视为服务社会,而不是随机绑定的缓存实例,那么单例就很有意义。

单例是服务访问点。紧密绑定的功能库的公共接口,可能隐藏了非常复杂的内部体系结构。

因此,我认为单身人士与工厂没有什么不同。单例可以传入构造函数参数。它可以由某些上下文创建,例如,该上下文知道如何根据所有可能的选择机制来解析默认打印机。为了进行测试,您可以插入自己的模拟。因此它可以非常灵活。

当我执行并需要一些功能时,密钥位于程序的内部,因此我可以完全放心地访问该单例服务已就绪并可以使用。当必须在状态机中启动的进程中有多个线程必须被视为就绪时,这才是关键。

通常,我会包装XxxService类,并在Xxx类周围包装单例。单例根本不在Xxx类中,它被分离为另一个类XxxService。这是因为Xxx可以有多个实例,尽管不太可能,但是我们仍然希望每个系统上都可以全局访问一个Xxx实例。 XxxService提供了很好的关注点分离。 Xxx不必强制执行单例策略,但是我们可以在需要时使用Xxx作为单例。

就像是:

//XxxService.h:
/**
 * Provide singleton wrapper for Xxx object. This wrapper
 * can be autogenerated so is not made part of the object.
 */

#include "Xxx/Xxx.h"


class XxxService
{
    public:
    /**
     * Return a Xxx object as a singleton. The double check
     * singleton algorithm is used. A 0 return means there was
     * an error. Developers should use this as the access point to
     * get the Xxx object.
     *
     * <PRE>
     * @@ #include "Xxx/XxxService.h"
     * @@ Xxx* xxx= XxxService::Singleton();
     * <PRE>
     */

     static Xxx*     Singleton();

     private:
         static Mutex  mProtection;
};


//XxxService.cpp:

#include "Xxx/XxxService.h"                   // class implemented
#include "LockGuard.h"     

// CLASS SCOPE
//
Mutex XxxService::mProtection;

Xxx* XxxService::Singleton()
{
    static Xxx* singleton;  // the variable holding the singleton

    // First check to see if the singleton has been created.
    //
    if (singleton == 0)
    {
        // Block all but the first creator.
        //
        LockGuard lock(mProtection);

        // Check again just in case someone had created it
        // while we were blocked.
        //
        if (singleton == 0)
        {
            // Create the singleton Xxx object. It's assigned
            // to a temporary so other accessors don't see
            // the singleton as created before it really is.
            //
            Xxx* inprocess_singleton= new Xxx;

            // Move the singleton to state online so we know that is has
            // been created and it ready for use.
            //
            if (inprocess_singleton->MoveOnline())
            {
                LOG(0, "XxxService:Service: FAIL MoveOnline");
                return 0;
            }

            // Wait until the module says it's in online state.
            //
            if (inprocess_singleton->WaitTil(Module::MODULE_STATE_ONLINE))
            {
                LOG(0, "XxxService:Service: FAIL move to online");
                return 0;
            }

            // The singleton is created successfully so assign it.
            //
            singleton= inprocess_singleton;


        }// still not created
    }// not created

    // Return the created singleton.
    //
    return singleton;

}// Singleton  
3
Todd Hoff

让每个屏幕都使用其构造函数中的Manager。

启动应用程序时,您将创建一个管理器实例并将其传递。

这称为“控制反转”,它使您可以在配置更改和测试中更换控制器。此外,您可以并行运行应用程序的多个实例或应用程序的多个部分(适合测试!)。最后,您的经理将死于其拥有的对象(启动类)。

因此,您的应用就像树一样构造,上面的东西拥有下面使用的所有东西。不要实现像网格这样的应用程序,每个人都认识每个人,并通过全局方法找到彼此。

1
Alexander Torstling

IMO,您的例子听起来还不错。我建议分解如下:为每个(和后面的)数据对象缓存对象;缓存对象和db访问器对象具有相同的接口。这样就可以在代码内外交换缓存。另外,它还提供了一条简单的扩展路线。

图形:

DB
|
DB Accessor for OBJ A
| 
Cache for OBJ A
|
OBJ A Client requesting

DB访问器和缓存可以从相同的对象或鸭子类型继承为看起来像相同的对象,无论如何。只要您可以插入/编译/测试,它仍然可以工作。

这样可以使事情解耦,因此您无需添加和修改某些Uber-Cache对象就可以添加新的缓存。 YMMV。 IANAL。等等。

1
Paul Nathan

第一个问题,您在应用程序中发现很多错误吗?也许忘记更新缓存,或者缓存不良或发现难以更改? (我记得一个应用程序不会更改大小,除非您也更改了颜色...不过,您可以更改颜色并保持大小)。

您要做的是拥有该课程,但要删除所有静态成员。 Ok这不是必要的,但是我推荐。实际上,您只是像普通类一样初始化类并通过指针即可。不要再说ClassIWant.APtr()。LetMeChange.ANYTHINGATALL() .andhave_no_structure()

它的工作更多,但实际上,它的混乱更少。在某些您不应该更改的地方,由于它不再具有全局性,因此您现在无法更改。我所有的经理班都是普通班,就那样对待。

1
user2528

派对晚了一点,但是无论如何。

就像其他任何东西一样,Singleton是工具箱中的工具。希望您的工具箱中不仅有一把锤子。

考虑一下:

public void DoSomething()
{
    MySingleton.Instance.Work();
}

public void DoSomething(MySingleton singleton)
{
    singleton.Work();
}
DoSomething(MySingleton.instance);

第一种情况导致高耦合等;据我所知,第二种方式没有问题@Aaronaught正在描述。有关如何使用它的所有信息。

1
Evgeni