[学习笔记]设计模式之Singleton

时间:2024-03-31 21:33:02

写在前面

为方便读者,本文已添加至索引

在前几篇笔记中,我们有了解了部分对象创建型模式,包括Builder(建造者)Abstract Factory(抽象工厂)Factory Method(工厂方法),今天我们要接触到的是另一种对象创建型模式,既简单又重要的:Singleton(单例)模式。

对一些类来说,只有一个实例是很重要的。比如说,一个软件系统中,应该只有一个窗口管理器;通信设备中,每张板卡上唯一的端口管理器;一个数字滤波器只能有一个A/D转换器。此外,这些唯一的实例还有一个特点是易于访问。我们知道,用一个全局变量可以使得一个对象可以容易被访问,但它并不能防止我们实例化多个对象,同时也会污染命名空间,并非最佳选择。那么如何才能保证一个类只有一个实例并且这个实例易于被访问呢?让我们来了解一下Singleton模式吧!

要点梳理

  • 目的分类
    • 对象创建型模式
  • 范围准则
    • 对象(该模式处理对象间的关系,这些关系在运行时刻是可以变化的,更具动态性)
  • 主要功能
    • 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
  • 适用情况
    • 当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。
    • 当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时。
  • 参与部分
    • Singleton:定义一个Instance操作,允许客户访问它的唯一实例。Instance是一个类操作(即C++中的一个静态成员函数)。
  • 协作过程
    • 客户只能通过Singleton的Instance操作访问一个Singleton的实例

示例分析 - 泰坦,世界管理者

让我们重新回到时の魔导士的故事世界中去。在Builder模式一节中,他曾利用WorldCreator来创造平行世界(如果读者从这里开始觉得有点不知所云,可以利用前文的传送门翻阅一下之前的笔记),但是为了使世界更加丰富多彩,魔导士不得不亲力亲为地去设计并改造每一处被创建的世界(譬如给白雪公主一行提供的美食工厂等),这无疑是很巨大的工作量。“如果能有人肯帮我做这些琐事,那么我可以多花点时间做些更美妙的研究啦。”魔导士突然灵光一闪,他想到了组建一个议会。在每一个平行世界中,组建起一个足够强大且独一无二的议会,并让它担任起改造世界,维持生态系统良性运转的重任。时の魔导士将它命名为WorldMgr(世界管理者):

 class WorldMgr {
public:
static WorldMgr* getInstance(); //Existing interface goes here
protected:
WorldMgr();
private:
static WorldMgr* _instance;
}

以及它的一个简单实现:

 WorldMgr* WorldMgr::_instance = ;

 WorldMgr* WorldMgr::getInstance() {
if (_instance == ) {
_instance = new WorldMgr();
}
return _instance;
}

为了能更好地理解Singleton模式,我们必须详细地解释下上面的代码部分。首先我们利用到静态成员_instance来保存唯一的实例,同时采用静态的getInstance操作提供一个外部访问实例的唯一接口,以此实现了一个Singleton类。其次,我们对构造器加以了保护,以防止WorldMgr被意外的实例化,因为意外的实例化可能会导致多个实例。

正是Singleton模式的引入,使得我们的WorldMgr类可以在平行世界的其他活动中被轻松调用(只需WorldMgr::getInstance() 即可返回它唯一的实例)。但这还不足以让时の魔导士彻底松一口气。作为一个议会,WorldMgr统管世界的方方面面,未免有点太至高无上了。所谓权利的完全集中可能会导致彻底的腐败,倘若魔兽世界中只有一头巨龙--死亡之翼,那么它的堕落将导致艾泽拉斯大陆不复存在。于是,时の魔导士需要一些个体来分摊这份权利,以使得他们之间相互制约、相互平衡。他顺势创造了Titan(泰坦),世界管理者议会的继承类:

掌管生命和死亡的 Iapetus

 class Iapetus : public WorldMgr {
protected:
Iapetus();
}

掌管海洋的 Oceanus

 class Oceanus : public WorldMgr {
protected:
Oceanus();
}

掌管天空的 Cronus

 class Cronus : public WorldMgr {
protected:
Cronus();
}

掌管记忆的 Mnemosyne

 class Mnemosyne : public WorldMgr {
protected:
Mnemosyne();
}

等等等等……

既然有这么多子类,如何在getInstance操作中去选择不同的泰坦们呢?这里,我们可以用环境变量的方法来选择。我们假定一个环境变量指定了Titan的名字:

 WorldMgr* WorldMgr::getInstance() {
if (_instance == ) {
const char* name = getenv("TITAN"); if (strcmp(name, "Iapetus")) {
_instance = new Iapetus();
}
else if (strcmp(name, "Oceanus")) {
_instance = new Oceanus();
}
else if (strcmp(name, "Cronus")) {
_instance = new Cronus();
}
else if (strcmp(name, "Mnemosyne")) {
_instance = new Mnemosyne();
}
// ... other Titans
else {
_instance = new WorldMgr();
}
}
return _instance;
}

不过这个方法的问题就在于,无论何时要定义一个新的Titan(即WorldMgr的子类),getInstance函数都必须被修改。于是我们可以采用另一种注册表的方法来动态链接。让我们看看改变后的WorldMgr:

 class WorldMgr {
public:
static void Register(const char* name, WorldMgr*);
static WorldMgr* getInstance(); //Existing interface goes here
protected:
WorldMgr();
static WorldMgr* Lookup(const char* name);
private:
static WorldMgr* _instance;
static List<NamePair>* _registry;
}

注意我们高亮了新添加的一些语句。Register以给定的泰坦名字注册WorldMgr实例。为保证注册表简单,我们将让它存储一列NamePair对象。每个NamePair将一个名字映射到一个单例。Lookup操作根据给定单件的名字进行查找。我们假定一个环境变量指定了所需要的单件的名字:

 WorldMgr* WorldMgr::_instance = ;

 WorldMgr* WorldMgr::getInstance() {
if (_instance == ) {
const char* name = getenv("TITAN");
// Lookup returns 0 if there's no such WorldMgr;
_instance = Lookup(name);
}
return _instance;
}

那么又在何处让Titan注册自己呢?一种可能是在构造器中,让我们以Mnemosyne为例:

 Mnemosyne::Mnemosyne() {
// ...
WorldMgr::Register("Mnemosyne", this);
}

当然,除非实例化类否则这个构造器不会被调用。我们可以在包含Mnemosyne实现的文件中定义:

static Mnemosyne theWorldMgr;

如此一来WorldMgr类不再负责创建单例。它的主要职责是使得供选择的单例对象在系统中可以被访问。但是这个静态对象方法还是有一个潜在的缺点:也就是所有可能的WorldMgr子类的实例都必须被创建,否则它们不会被注册。 不过对于时の魔导士而言,这点就不重要啦,因为他确实需要创建所有的Titan们,并让他们开始担负起维护世界运作的重任。

特点总结

跟随魔导士的步伐,我们见证了一个伟大议会WorldMgr的诞生,同时它也是Singleton模式的很好实例。我们可以看到,Singleton有很多优点:

  1. 对唯一实例的受控访问。因为Singleton类封装它的唯一实例,所以它可以严格地控制客户怎样以及何时访问它。
  2. 缩小命名空间。Singleton模式是对全局变量的一种改进。它避免了那些存储唯一实例的全局变量污染名空间。
  3. 允许对操作和表示的精化。Singleton类可以有子类,而且用这个扩展类的实例来配置一个应用是很容易的。我们可以用所需要的类的实例在运行时刻配置应用。
  4. 允许可变数目的实例。这个模式使得我们易于改变想法,并允许Singleton类的多个实例。此外,我们可以用相同的方法来控制应用所使用的实例的数目。只有允许访问Singleton实例的操作需要改变。
  5. 比类操作更灵活。另一种封装单件功能的方式是使用类操作(即C++中的静态成员函数)。但它难以改变设计以允许一个类有多个实例。此外,C++中的静态成员函数不是虚函数,因此子类不能多态的重定义它们。

写在最后

今天的笔记就到这里了,欢迎大家批评指正!如果觉得可以的话,好文推荐一下,我会非常感谢的!