面向对象的设计模式系列之一:单件模式(Singleton)
大家好,由于工作繁忙导致很久没有更新博客了。结合我一直以来的工作经历和项目经验,谈一下个人关于设计模式的理解。其实这个话题是非常大的,也非常深入的,可能不能一时半会能全部展现整个设计模式的精髓。因此我的建议是:首先了解每种设计模式的应用场景和基本框架,模式只有在特定的环境下才能发挥强大的作用,其次我们延伸对设计模式的应用,因此模式不能死板硬套,否则就会变成为了模式而模式,那就非常不合适的。说到这,让我想起来一句话:在合适的时间,合适的地点遇到合适的人,也许才是你一生的挚爱。对设计模式而言:在特定的应用场景,怀着特定的意图并融合以及适当扩展设计模式,也许才是项目使用设计模式的王道。在讲解Sington模式之前,我顺便谈一下设计模式的分类:
从目的来看,分为创建型(Creational)模式: 负责对象的创建;结构型(Structural)模式:负责类与对象之间的组合;行为型(Behavioral)模式: 负责类与对象之间通信的职责分配。
从范围来看,分为类模式:处理类与子类的静态关系;对象模式:处理对象间的动态关系。
接下来我们开始进入Singleton模式的讲解了。
设计意图:保证一个类仅有一个实例,并提供一个访问该实例的全局访问点。
应用场景:在软件系统中,经常有一些特殊的类,必须保证在系统中有唯一的实例存在,才能保证整个逻辑正确性、用户无需修改任何代码即可使用。
实际案例: 大家通过MSN发送、接受实时消息时,所有请求必须通过集中的代理服务器的Http端口发送,虽然在那台代理服务器的80端口已经安排了很多工作,比如网页浏览以及B2B的Web Service调用,但却需要保证一个端口在同一时间内只能为一个进程打开。假设某个进程打开后没有及时关闭Http端口,导致其他任务无法及时的使用,这时这个进程则是代理服务器的唯一Singleton(单件)。同样,数据库的自增字段原理也类似: 假设A、B在不同的数据库会话中(无论是专用服务器还是共享服务器),自增字段在A中出现1000,1001,1002,....,9999的同时,B也获得同样的结果,完全与A保持同步,那这时自增字段的序号生成部分也是一个Singleton(单件)。
那如何才能绕过常规的构造器,提供一种机制来保证类只有一个实例呢?我们说这应该是类的责任,而不是使用者的责任。必须满足以下三点:
(1)私有或保护构造函数以防止外部实例化。
(2)保存唯一实例的静态的私有变量。
(3)初始化并获得唯一实例的静态属性或方法。
首先,我们来看一下单线程下Singleton模式的实现。
public class Singleton
{
//1.保存唯一实例的静态的私有变量
private static Singleton instance;
//2.私有构造函数以防止外部实例化
private Singleton()
{
}
//3.初始化并获得唯一实例的静态属性或方法
public static Singleton Instance
{
get
{
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
}
}
我们分析一下单线程的Singleton模式的几个要点:
(1)Singleton的实例构造函数也能以protected的方式允许子类派生。
(2)一般不支持ICloneable接口,因为这可能导致多个实例产生,与Singleton模式的初衷相违背。
(3)一般不支持序列化,因为这可能导致多个实例产生,与Singleton模式的初衷相违背。
(4)只考虑了对象的创建,而未考虑对象的销毁。但垃圾回收(GC)已经帮我们搭理了一切。
(5)不能应对多线程环境,使用单线程的Singleton模式在多线程下仍然可能产生多个对象。
为什么这样讲呢?假设在多线程下,两个不同的线程同时创建Singleton对象时,A对if(instance==null)的判断还未完成的同时,B也加入了判断,此时就可能导致A、B自产生一个Singleton对象,这显然使得这种实现具有局限性。接下来继续看多线程下Singleton模式的实现。
public class Singleton
{
//1.保存唯一实例的静态的私有变量
private static volatile Singleton instance = null;
private static object lockHelper = new object();
//2.私有构造函数以防止外部实例化
private Singleton()
{
}
//3.初始化并获得唯一实例的静态属性或方法
public static Singleton Instance
{
get
{
//double check操作
if (instance == null)
{
lock (lockHelper)
{
if (instance == null)
{
instance = new Singleton();
}
}
}
return instance;
}
}
}
我们发现多线程下的Singleton模式实现增加了一个辅助对象lockHelper和double check操作,其次增加了volatile对私有实例变量的修饰。double check操作保证了多个线程下由于lock为内层if(instance==null)构建了一个线程安全的小环境。而外层的if(instance==null)判断则可以防止多线程每次都会去锁定辅助对象而造成性能下降。而volatile则保证不受编译器优化,确保该字段在任何时候都能获得最新的值。即在被lock之后,如果还未完成new Singleton()之前,instance都会保持null。 虽然double check实现了多线程下Singleton对象的创建,但依我们经验而言,为什么不把对象的构建过程放在静态构造函数里呢?其中一个原因就是编译器会"好心地"将静态成员instance构造次序重排,而.NET能使用指定的方法明确规定静态成员的构建次序,即静态构造函数必须在任何静态成员的初始化之前执行。我们接下来使用readonly改造double check的实现方式。
public class Singleton
{
public static readonly Singleton Instance = new Singleton();
private Singleton() {}
}
采用readonly方式实现是如此之简洁,而且可以保证多线程下Singleton的唯一实例,我将一一来解释原因。首先公有静态实例会在类第一次使用时被创建,并省去了Lazy构造过程和避免了编译器优化而带来的次序重排。其次私有构造函数防止了外部初始化。至于如何保证多线程下Singleton实例的唯一性,可以从IL代码中得以体现。使用idasm命令获得IL代码。
.class public auto ansi beforefieldinit DesignPattern.SingletonPattern.Static.Singleton
extends [mscorlib]System.Object
{
.field public static initonly class DesignPattern.SingletonPattern.Static.Singleton Instance
.method private hidebysig specialname rtspecialname static
void .cctor() cil managed
{
// Code size 11 (0xb)
.maxstack 8
IL_0000: newobj instance void DesignPattern.SingletonPattern.Static.Singleton::.ctor()
IL_0005: stsfld class DesignPattern.SingletonPattern.Static.Singleton DesignPattern.SingletonPattern.Static.Singleton::Instance
IL_000a: ret
} // end of method Singleton::.cctor
} // end of class DesignPattern.SingletonPattern.Static.Singleton
这里有一个beforefieldinit修饰符告诉CLR只有这里的静态成员(字段)在静态构造函数执行后才生效,即使有很多线程同时访问Instance,但都需等静态构造函数执行完成后才能使用。虽然Instance被定义为静态公有的,但却是只读的,一旦被创建将无法被任何线程改变,也就无需double check的检查了。我们也知道,对于每一个类静态构造函数只有一个且只能执行一次,接下来我们将readonly方式的内部原理代码给出来供大家参考,以方便理解我们刚才讲到的这些内容。
//与readonly直接实例化完全一致
public class Singleton
{
public static readonly Singleton Instance = null;
static Singleton()
{
Instance = new Singleton();
}
private Singleton() { }
}
谈到这里,对Singleton的基本结构也有了大致的了解,我们接下来以计数器为例来说明如何应用Singleton模式。
public class Counter
{
public static readonly Counter Instance = new Counter();
private Counter() { }
private int value;
public int Next
{
get { return ++value; }
}
public void Reset()
{
value = 0;
}
}
我们建立单元测试类,以计数器测试是否唯一。
[TestClass]
public class CounterTest
{
[TestMethod]
public void TestSingleton()
{
Counter.Instance.Reset();
var c1 = Counter.Instance;
var c2 = Counter.Instance;
Assert.AreEqual<Counter>(c1, c2);
Assert.AreEqual<int>(1, c1.Next);
Assert.AreEqual<int>(2, c2.Next);
Assert.AreEqual<int>(3, c1.Next);
}
}
我们看出c1,c2虽然创建了两次,但却是同一对象,c1在计数的同时c2也参与了计数,即系统中现在只存在唯一的实例。通过以上实例说明采用readonly方式实现Singleton模式虽如此简洁,但有一个缺点,无法实现带参的构造函数,如果要实现这个还必须回到以前的实现方式。
public class Singleton
{
//1.保存唯一实例的静态的私有变量
private static volatile Singleton instance = null;
private static object lockHelper = new object();
private string message = String.Empty;
//2.私有构造函数以防止外部实例化
private Singleton(string message)
{
this.message = message;
}
//3.初始化并获得唯一实例的静态属性或方法
public static Singleton Instance
{
get
{
//double check操作
if (instance == null)
{
lock (lockHelper)
{
if (instance == null)
{
var date = DateTime.Now.DayOfWeek == DayOfWeek.Saturday || DateTime.Now.DayOfWeek == DayOfWeek.Sunday;
instance = new Singleton(date ? "weekday" : "workday");
}
}
}
return instance;
}
}
public string Message
{
get { return this.message; }
}
}
但看看上面的代码直接硬编码过于僵化,将构造函数的参数直接具体化在程序中了,如果要改进这种方式,可以将属性改成方法或者配置文件实现。
public static Singleton GetInstance(string message)
{
//double check操作
if (instance == null)
{
lock (lockHelper)
{
if (instance == null)
{
//var message = ConfigurationManager.AppSettings["SingletonMessage"];
instance = new Singleton(message);
}
}
}
return instance;
}
到现在,我们明确Singleton模式定义了唯一的实例数量,但其根本目的在于保持操作的一致性,如更新计数器、更新整个应用程序的配置信息等。但随着业务的不断发展,需要Singleton产生指定的N个实例,可能有人会问:那这不是和Singleton产生唯一实例相违背了吗?和一般的new()有何区别?其实还是有不同点,首先来说这是对Singleton模式扩展,我们姑且称为Singleton-N模式。其次,这里的N是一个相对固定的数量,例如N=5,那产生1,2,3,4个实例时没有问题,5也可以,超过5个则已经超出系统容量了。我们来看以下如何实现这种模式。我们来分析以下如何利用以前的实现方式来解决Singleton-N模式,首先经典的Lazy构造方式不适合并发环境,PASS;采用readonly的静态构造函数无法构建对象数组实例,PASS;看来double check还有改造的空间,为了不打破这种方式,并且依据职责单一原则,我们建立辅助类WorkItemCollection,必须满足:
(1)本身应该是一个可以存在N个实例的集合类型。
(2)集合中的每个实例都能通过状态标识是否处于忙碌或闲暇,同时根据外部的反馈来修正自己的状态。
(3)可以告诉外界是否还有空位,以释放空间来创建对象。
从某种程度上来说,Singleton-N模式是ObjectPool模式的一个预备模式。它除了完成一般创建型模式的new()之外,还要控制检查实例的数量。那我们来看看如何实现吧。
/// <summary>
/// 实例的执行状态
/// </summary>
public enum Status
{
Busy,//实例被客户程序占用
Free//实例没有被客户程序占用
}
public interface IWorkItem
{
Status Status { get; set; }
void DeActivate();
}
/// <summary>
/// 定义Singleton实例集合
/// </summary>
/// <typeparam name="T"></typeparam>
public class WorkItemCollection<T> where T : class, IWorkItem
{
protected int max;//定义最多保存的实例数量
protected IList<T> items = new List<T>();
public WorkItemCollection(int max)
{
this.max = max;
}
/// <summary>
/// 外部获得T类型的入口
/// </summary>
/// <returns></returns>
public virtual T GetWorkItem()
{
if (items == null || items.Count == 0) return null;
//可能的话返回一个现成实例
foreach (T item in items)
{
if (item.Status == Status.Free)
{
item.Status = Status.Busy;
return item;
}
}
return null;//如果没有现成的实例则返回null
}
/// <summary>
/// 添加一个实例,默认状态为闲置
/// </summary>
/// <param name="item"></param>
public virtual void Add(T item)
{
if (item == null) throw new ArgumentNullException("item is null");
if (!IsAddNewInstance) throw new OverflowException();
item.Status = Status.Free;
items.Add(item);
}
/// <summary>
/// 是否还能添加新的实例
/// </summary>
public virtual bool IsAddNewInstance
{
get { return items.Count < max; }
}
}
public class SingletonN : IWorkItem
{
private static readonly int MaxInstances = 2;//最多能产生的实例个数
private Status status = Status.Free;//初始状态
public Status Status
{
get { return this.status; }
set { this.status = value; }
}
public void DeActivate()
{
this.status = Status.Free;
}
//实现单件模式
private static WorkItemCollection<SingletonN> collection = new WorkItemCollection<SingletonN>(MaxInstances);
private static object lockHelper = new object();
private SingletonN() { }
//在Singleton主体框架不变下,引入集合实现多个实例的对象管理
public static SingletonN Instance
{
get
{
var instance = collection.GetWorkItem();
if (instance == null)
{
lock (lockHelper)
{
if (instance == null)
{
if (!collection.IsAddNewInstance) return null;
instance = new SingletonN();
collection.Add(instance);
}
}
}
instance.status = Status.Busy;//激活使用
return instance;
}
}
}
我们建立单元测试,以便验证当前Singleton-N模式实现的正确性。
[TestClass]
public class SingletonTest
{
[TestMethod]
public void TestSingletonN()
{
var c1 = SingletonN.Instance;
var c2 = SingletonN.Instance;
var c3 = SingletonN.Instance;
Assert.IsNull(c3);//c3因为没有空间而无法实例化
//证明c1,c2是两个不同的实例
Assert.AreNotEqual<int>(c1.GetHashCode(), c2.GetHashCode());
c1.DeActivate();//将c1的空间释放掉
c3 = SingletonN.Instance;//此时因为有了空间,c3可以获得空间实例化
Assert.IsNotNull(c3);
c2.DeActivate();
Assert.IsNotNull(c3);//此时仍然有空间,c3还是存在的
//c3虽然获得了新的空间,但该空间是属于之前c1或c2释放的
Assert.IsTrue(c3.GetHashCode() == c1.GetHashCode() || c3.GetHashCode() == c2.GetHashCode());
}
}
在注释中已经说明了一切,到这里,我想大家对Singleton模式的理解可能有很多新的认识,给人的感觉就是模式并不是一成不变的,而是千变万化的,但万变不离需求。我们只要掌握住了需求,并适当的扩展模式本身的实现,才能更好的应用设计模式。此外,Singleton模式也并不总是单独使用,它经常结合和其他模式一起使用,例如工厂模式中的工厂对象就常被设计成单件模式,此外外观模式(Facade)中的外观对象也经常被设计成单件模式,这些都是很好的范例,我将在以后的文章中谈到,希望大家可以从中捕捉到新的思想。在这里,我将源代码上传,大家可以打开运行,谢谢。