DM:单件模式(Singleton)——一个类的唯一实例

Singleton(单例)——对象创建型模式

1、意图

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

2、动机

对一些类来说,只有一个实例是很重要的。虽然系统中可以有许多打印机,但却只应该有一个打印假脱机(printer spooler),只应该有一个文件系统和一个窗口管理器。一个数字滤波器只能有一个A/D转换器。一个会计系统只能专用于一个公司。

如何才能保证一个类只有一个实例并且这个实例易于被访问呢?一个全局变量使得一个对象可以被访问,但它不能防止你实例化多个对象。

一个更好的办法是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建(通过截取创建新对象的请求),并且它可以提供一个访问该实例的方法。这就是Singleton模式

3、适用性

在下面的情况下可以使用Singleton模式

(1)当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。

(2)当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时。

4、结构

5、参与者

Singleton

1)定义一个Instance操作,允许客户访问它的唯一实例。Instance是一个类操作(即Smalltalk中的一个类方法和C++中的一个静态成员函数)。

2)可能负责创建它自己的唯一实例。

6、协作

客户只能通过SingletonInstance操作访问一个Singleton的实例

7、效果

Singleton模式的优点:

1)对唯一实例的受控访问。因为Singleton类封装它的唯一实例,所以它可以严格的控制客户怎样以及何时访问它。

2)缩小名空间。Singleton模式是对全局变量的一种改进。它避免了那些存储唯一实例的全局变量污染名空间。

3)允许对操作和表示的精化。Singleton类可以有子类,而且用这个扩展类的实例来配置一个应用是很容易的。你可以用你所需要的类的实例在运行时刻配置应用。

4)允许可变数目的实例。这个模式使得你易于改变你的想法,并允许Singleton类的多个实例。此外,你可以用相同的方法来控制应用所使用的实例的数目。只有允许访问Singleton实例的操作需要改变。

5)比类操作更灵活。另一种封装单例功能的方式是使用类操作(即C++中的静态成员函数或者是Smalltalk中的类方法)。但这两种语言技术都难以改变设计以允许一个类有多个实例。此外,C++中的静态成员函数不是虚函数,因此子类不能多态的重定义它们。

8、实现

下面是使用Singleton模式时所要考虑的实现问题:

1)保证一个唯一的实例

Singleton模式使得这个唯一实例是类的一般实例,但该类被写成只有一个实例能被创建。做到这一点的一个常用方法是将创建这个实例的操作隐藏在一个类操作(即一个静态成员函数或者是一个类方法)后面,由它保证只有一个实例被创建。这个操作可以访问保存唯一实例的变量,而且它可以保证这个变量在返回值之前用这个唯一实例初始化。这种方法保证了单例在它的首次使用前被创建和使用。

在C++中,你可以用Singleton类的静态成员函数Instance来定义这个类操作。Singleton还定义了一个静态成员变量_instance,它包含了一个指向它的唯一实例的指针。

Singleton类定义如下

class Singleton{
  public:
    static Singleton* Instance();
  protected:
    Singleton();
  private:
    static Singleton* _instance;
};

相应的实现是

Singleton* Singleton::_instance=0;
Singleton* Singleton::Instance(){
  if(_instance==0){
    _instance=new Singleton;
  }
  return _instance;
}

客户仅通过Instance成员函数访问这个单例。变量_instance初始化为0,而静态成员函数Instance返回该变量值,如果其值为0则用唯一实例初始化它。Instance使用惰性(lazy)初始化;它的返回值直到被第一次访问时才创建和保存。

注意,构造器是保护型的。试图直接实例化Singleton的客户将得到一个编译时的错误信息。这就保证了仅有一个实例可以被创建。

此外,因为_instance是一个指向Singleton对象的指针,Instance成员函数可以将一个指向Singleton的子类的指针赋给这个变量。

关于C++的实现还有一点需要注意。将单例定义为一个全局或静态的对象,然后依赖于自动的初始化,这是不够的。原因有如下三个:

a)不能保证静态对象只有一个实例会被声明。

b)可能没有足够的信息在静态初始化时实例化每一个单例。单例可能需要在程序执行中稍后被计算出来的值。

c)C++没有定义转换单元(translation unit)上全局对象的构造器的调用顺序。这就意味着单例之间不存在依赖关系;如果有,那么错误将是不可避免的。

使用全局/静态对象的实现方法还有另一个缺点,它使得所有单例无论用到与否都要被创建。使用静态成员函数避免了所有这些问题。

2)创建SIngleton类的子类

主要问题与其说是定义子类不如说是建立它的唯一实例,这样客户就可以使用它。事实上,指向单例实例的变量必须用子类的实例进行初始化。最简单的技术是在Singleton的Instance操作中决定你想使用的是哪一个单例。

另一个选择Singleton的子类的方法是将Instance的实现从父类(即MazeFactory)中分离出来并将它放入子类。这就允许C++程序员在链接时刻决定单例的类(即通过链入一个包含不同实现的对象文件),但对单例的客户则隐藏这一点。

链接的方法在链接时刻确定了单例类的选择,这使得难以在运行时刻选择单例类。使用条件语句来决定子类更加灵活一些,但这硬性限定(hard-write)了可能的SIngleton类的集合。这两种方法不是在所有的情况都足够灵活的。

一个更灵活的方法是使用一个单例注册表(registry of Singleton)。可能的Singleton类的集合不是由Instance定义的,Singleton类可以根据名字在一个众所周知的注册表中注册它们的单例实例。

这个注册表在字符串名字和单例之间建立映射。当Instance需要一个单例时,它参考注册表,根据名字请求单例。

注册表查询相应的单例(如果存在的话)并返回它。这个方法使得Instance不再需要知道所有可能的Singleton类或实例。它所需要的只是所有Singleton类的一个公共的接口,该接口包括了对注册表的操作:

class Singleton{
  public:
    static void Register(const chat* name,Singleton*);
    static Singleton* Instance();
  protected:
    static Singleton* Lookup(const char* name);
  private:
    static Singleton* _instance;
    static List<NameSingletonPair>* _registry;
};

Register以给定的名字注册Singleton实例。为保证注册表简单,现将让它存储一列NameSingletonPair对象。每个NameSingletonPair将一个名字映射到一个单例。Lookup操作根据给定单例的名字进行查找。假定一个环境变量指定了所需要的单例的名字,如下:

Singleton* Singleton::Instance(){
  if(_instance==0){
    const char* singletonName=getenv("SINGLETON");
    // user or environment supplies this at startup
    
    _instance=Lookup(singletonName);
    // Lookup returns 0 if there's no such singleton
  }
  return _instance;
}

Singleton类在何处注册它们自己?一种可能是在它们的构造器中。例如,MySingleton子类可以像下面这样做:

MySingleton::MySingleton(){
  //...
  Singleton::Register("MySingleton",this);
}

当然,除非是实例化类,否则,这个构造器是不会被调用的,这正反映了SIngleton模式试图解决的问题!在C++中,可以定义MySIngleton的一个静态实例来避免这个问题。例如,可以在包含MySingleton实现的文件中定义:
static MySingleton theSingleton;

Singleton类不再负责创建单例。它的主要职责是使得供选择的单例对象在系统中可以被访问。静态对象方法还是有一个潜在的缺点——也就是所有可能的Singleton子类的实例都必须被创建,否则它们不会被注册。

9、代码示例

定义一个MazeFactory类用于描述一个迷宫(maze:迷宫)。MazeFactory定义了一个建造迷宫的不同部件的接口。子类可以重定义这些操作以返回特定产品类的实例,如用BombedWall对象代替普通的Wall对象。

此处相关的问题是Maze应用仅需要迷宫工厂的一个实例,且这个实例对建造迷宫任何部件的代码都是可用的。这样就引入了Singleton模式。将MazeFactory作为单例,无需借助全局变量就可使迷宫对象具有全局可访问性。

为简单起见,假定不会生成MazeFactory的子类。通过增加静态的Instance操作和静态的用以保存唯一实例的成员_instance,从而在C++中生成一个Singleton类。还必须保护构造器以防止意外的实例化,因为意外的实例化可能会导致多个实例。

class MazeFactory{
  public:
    static MazeFactory* Instance();

    //existing interface goes here
  protected:
    MazeFactory();

  private:
    static MazeFactory* _instance;
};

相应的实现是:

MazeFactory* MazeFactory::_instance=0;
MazeFactory* MazeFactory::Instance(){
  if(_instance==0){
    _instance=new MazeFactory;
  }
  return _instance;
}

现在考虑当存在MazeFactory的多个子类,而且应用必须决定使用哪一个子类时的情况。将通过环境变量选择迷宫的种类并根据环境变量的值增加代码用于实例化适当的MazeFactory子类。Instance操作是增加这些代码的好地方,因为它已经实例化了MazeFactory:

MazeFactory* MazeFactory::Instance(){
  if(_instance==0){
    const char* mazeStyle=getenv("MAZESTYLE");

    if(strcmp(mazeStyle,"bombed")==0){
      _instance=new BombedMazeFactory;
    }else if(strcmp(mazeStyle,"enchanted")==0){
      _instance=new EnchantedMazeFactory;
      
      //...other possible subclasses
    }else{
      _instance=new MazeFactory;
    }
  }
  return _instance;
}

注意,无论何时定义一个新的MazeFactory的子类,Instance都必须被修改。在这个应用中可能没什么关系,但对于定义在一个框架中的抽象工厂来说,这可能是一个问题。

一个可能的解决办法将是使用在实现一节中所描述过的注册表的方法。此处动态链接可能也很有用——它使得应用不需要装载那些用不着的子类。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值