[简介]
上期我们刊登了Jeffrey Richter的《探究Observer模式》,详细讲解了在微软架构下实现Observer模式的技术和经验。本期我们继续探讨设计模式方面的话题。Singleton通常被认为是最简单的设计模式,很多初学者都是通过它来了解设计模式的含义。然而,熟悉设计模式的技术人员都知道,要正确实现Singleton模式实际上是非常难的,涉及到很多技术细节。本文对于Singleton做了大胆深入的研究,并且探讨了C++、Java和C#中的Singleton实现。
在开发软件应用程序过程中,随着应用程序的开发,会出现重复性的模式。随着整个软件系统的开发,很多相同的模式会逐渐显现出来。
这种重复性模式概念在其他应用中是非常明显的。汽车制造就是一种此类应用。很多不同的汽车型号使用相同的子构件,包括大多数基本部件(例如,灯泡和紧固零件)以及较大的构件(例如,底盘和发动机)。
在住宅建筑中,重复性模式概念适用于螺丝和螺钉以及整体总体建筑物配电系统。无论组建的小组是为了开发新的汽车设计还是新的建筑物设计,它通常不必没有考虑到以前已解决的问题。如果设计和建筑住宅的小组必须重新构思和设计房子的每一个组成部分,则整个过程所花的时间比现在要长得多。门高或灯开关功能等许多设计决策(例如,门高或灯开关功能)很容易理解。房为满足给房子不同部分提供洗手功能的要求,房屋设计师不必重新设计和重新建造不同类型的输供水和蓄水设施,以便达到为房子不同部分提供洗手功能的要求:标准水槽以及标准的热水和冷水输入接头和排水输出接头是很容易理解非常常见的房屋建筑构件。可以将重复性模式概念反复应用于我们周围的几乎每样东西上,包括软件。
汽车和住宅建筑示例有助于在软件设计和构造中体现某些一般性的抽象概念。易于理解且明确定义的通用功能部件的概念是设计模式的源动力,它也是其他两篇设计模式文章探究工厂设计模式和探究观察者设计模式的重点。这些模式几乎涵盖了面向对象的软件设计的各个方面,包括对象创建、对象交互和对象生存期。在本文中,我们将讨论Singleton模式,它包含在创造性模式系列中。
创造性模式指示如何以及何时创建对象。很多实例需要只能通过创造性方法解决的特殊行为,而不是在创建实例后强制实施所需的行为。此类行为要求最好的例子之一包含在Singleton模式中。Singleton模式在《设计模式:可复用的面向对象软件的基础》这一经典参考书目中有正式的定义,该书的作者包括Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides(也称为四人组或GoF)。在设计模式中,此模式是最简单也是使用最广泛的模式之一。但是,正如我们将会看到的一样,在实现此模式时可能会出现一些问题。本文试图通过Singleton模式的多个早期实现来从头开始分析Singleton模式,以及如何在Microsoft .NET应用程序开发中发挥其最佳用途。
Singleton模式
按照设计模式中的定义,Singleton模式的用途是“ensure a class has only one instance, and provide a global point of access to it(确保每个类只有一个实例,并提供它的全局访问点)”。
它可以解决什么问题,或者换句话说,我们使用它的动机是什么?几乎在每个应用程序中,都需要有一个从中进行全局访问和维护某种类型数据的区域。在面向对象的(OO)系统中也有这种情况,在此类系统中,在任何给定时间只应运行一个类或某个类的一组预定义数量的实例。例如,当使用某个类来维护增量计数器时,此简单的计数器类需要跟踪在多个应用程序领域中使用的整数值。此类需要能够增加该计数器并返回当前的值。对于这种情况,所需的类行为应该仅使用一个类实例来维护该整数,而不是使用其它类实例来维护该整数。
最初,人们可能会试图将计数器类实例只作为静态全局变量来创建。这是一种通用的方法,但实际上只解决一部分问题;它解决了全局可访问性问题,但没有采取任何措施来确保在任何给定的时间只运行一个类实例。应该由类本身来负责只使用一个类实例,而不是由类用户来负责。应该始终不要让类用户来监视和控制运行的类实例的数量。
所需要的是使用某种方法来控制如何创建类实例,然后确保在任何给定的时间只创建一个类实例。这会确切地给我们提供所需的行为,并使客户端不必了解任何类细节。
逻辑模型
Singleton模型非常简单直观。(通常)只有一个Singleton实例。客户端通过一个已知的访问点来访问Singleton实例。在这种情况下,客户端是一个需要访问唯一Singleton实例的对象。图1以图形方式显示此关系。
物理模型
Singleton模式的物理模型也是非常简单的。但是,随着时间的推移,实现Singleton的方式也略有不同。让我们看一下原始的GoFSingleton实现。图2显示按设计模式所定义的原始Singleton模式的UML模型。
我们看到的是一个简单的类图表,显示有一个Singleton对象的私有静态属性以及返回此相同属性的公共方法Instance()。这实际上是Singleton的核心。还有其他一些属性和方法,用于说明在该类上允许执行的其他操作。为了便于此次讨论,让我们将重点放在实例属性和方法上。
客户端仅通过实例方法来访问任何Singleton实例。此处没有定义创建实例的方式。我们还希望能够控制如何以及何时创建实例。在OO开发中,通常可以在类的构造函数中最好地处理特殊对象的创建行为。这种情况也不例外。我们可以做的是,定义我们何时以及如何构造类实例,然后禁止任何客户端直接调用该构造函数。这是在Singleton构造中始终使用的方法。让我们看一下设计模式中的原始示例。通常,将下面所示的C++Singleton示例实现代码示例视为Singleton的默认实现。本示例已移植到很多其他编程语言中,通常它在任何地方的形式与此几乎相同。
C++Singleton示例实现代码
//Declaration
class Singleton{
public:
static Singleton* Instance();
protected:
Singleton();
private:
static Singleton* _instance;
}
// Implementation
Singleton* Singleton::_instance = 0;
Singleton* Singleton::Instance() {
if (_instance == 0) {
_instance = new Singleton;
}
return _instance;
}
让我们先花点时间分析一下此代码。该简单类有一个成员变量,此变量是指向该类自身的指针。注意,构造函数是受保护的,并且只有公共方法才是实例方法。在实例方法实现中,有一个控制块(if),它检查成员变量是否已初始化,如果没有的话,则创建一个新实例。控制块中这种惰性初始化意味着仅在第一次调用Instance()方法时初始化或创建Singleton实例。对于很多应用程序,这种方法效果很好。但对于多线程应用程序,这种方法证明具有潜在危险的副作用。如果两个线程同时进入控制块,则可能会创建该成员变量的两个实例。要解决这一问题,您可能想只将重要部分放在控制块周围以确保线程安全。如果您这样做,则将对实例方法的所有调用进行序列化处理,并且可能会对性能产生不利影响(取决于应用程序)。正是由于这个原因,创建了此模式的另一个版本,它使用某种称为双重检验机制的功能。下一个代码示例显示使用Java语法的双重检验锁定。
使用Java语法的双重检验锁定Singleton代码
//C++ port to Java
class Singleton
{
public staticSingletonInstance() {
if (_instance == null) {
synchronized (Class.forName("Singleton")) {
if (_instance == null) {
_instance = new Singleton();
}
}
}
return _instance;
}
protected Singleton() {}
private staticSingleton_instance = null;
}
在使用Java语法的双重检验锁定Singleton代码示例中,我们直接