单件模式的主要注意事项
上面的例子虽然简单,但实际使用中还需要考虑不要因为面向对象特性和新的开发平台特性,所导致的单件类型性质的变异。
- 不要实现ICloneable接口或者继承自其相关的子类,否则客户程序可以跳过已经隐蔽起来的类构造函数,导致私有的Singleton() { } 部分的实效。
using System; namespace VisionLogic.DesignPattern.Practice ...{ public class BaseEntity : ICloneable ...{ public object Clone() ...{ return this.MemberwiseClone(); } } public class Singleton : BaseEntity ...{ private static Singleton instance; private Singleton() ...{ } public static Singleton Instance ...{ get ...{ if (instance == null) ...{ instance = new Singleton(); instance.data = 0; } return instance; } } …. } }示例3
using System; using System.Diagnostics; using VisionLogic.DesignPattern.Practice; namespace VisionLogic.DesignPattern.Practice.Client ...{ class Program ...{ static void Main(string[] args) ...{ Singleton instance1 = Singleton.Instance; Singleton instance2 = (Singleton)instance1.Clone(); instance1.Data = 30; Trace.WriteLine(instance2.Data); instance2.Data = 15; Trace.WriteLine(instance1.Data); } } } 结果: 0 30
示例4
上面的示例说明通过ICloneable接口的克隆过程导致私有构造函数的失效,CLR通过内存结构的复制生成了一个新的实例,最终导致不同实例的引用操作结果的各自独立。
上面的单件实现虽然采取了无参数的私有构造函数,但是实际工程中对于第一个且唯一一个实例的构造可能会依赖于具体的参数,不过由于每个客户程序在真正使用时使用的都是既得的实例,因此实际上客户程序不应该把构造参数传递给唯一个实例的构造函数,实际的参数传递过程一般只能通过硬编码在单件类型的静态属性Instance中完成。考虑到代码的维护性问题,笔者建议保留实例的无参数构造函数,而把需要传递的参数通过配置系统传递到唯一实例的构造函数中,例如这里增加了三个配置值:
<configuration> <configSections> <section name="sampleSection" type="System.Configuration.SingleTagSectionHandler" /> </configSections> <sampleSection setting1="Value1" setting2="value two" setting3="third value" /> </configuration>
示例5
- 作为一个集中访问的单独实例,有时候常常需要对于自身数据进行更新,更新的方式可以通过外部显示调用或者单件实例对象自主更新方式两种方式完成。
如果准备采用外部调用,那么工程上最好通过增加一个接口(例如:名为 IRefershable)为应用中每个Singleton类型统一增加更新的可能。与上一个说明同理,这里刷新过程也最好统一设计为无参数的方法,待刷新所需要的数据源最好来自于一个配置文件或者来自运行环境。
using System; namespace VisionLogic.DesignPattern.Practice ...{ /**//// <summary> /// 为Singleton 对象增加可以更新的能力 /// </summary> interface IRefershable ...{ void Refersh(); } class Singleton : IRefershable ...{ ... } }
示例6
内部方式则是可以为其增加一个System.Threading.Timer或者通过配置文件的Watcher的回调代理完成,下面是一个增加了Timer的内部自发更新示例:
using System; namespace VisionLogic.DesignPattern.Practice ...{ public class Singleton ...{ private static Singleton instance; private System.Threading.Timer timer; private const int Interval = 2000; private Singleton() ...{ } public static Singleton Instance ...{ get ...{ if (instance == null) ...{ instance = new Singleton(); instance.timeStamp = System.DateTime.Now; instance.timer = new System.Threading.Timer( instance.RefershTimeStamp, null, 0, Interval); } return instance; } } /**//// <summary> /// 共享的实例数据 /// </summary> private DateTime timeStamp; public DateTime TimeStamp ...{ get ...{ return timeStamp; } } /**//// <summary> /// timer 的回调方法 /// </summary> /// <param name="state"></param> private void RefershTimeStamp(object state) ...{ timeStamp = System.DateTime.Now; } } }
示例7
- 对于较为大型的应用,一般部署上会考虑采用NLB集群,这样如果Singleton其中需要保存一些公共的内存对象(例如:计数器),但是由于在每个服务器节点上各自都有Singleton,所以总体来看Singleton并不“单件”,那么应用对于单一实例的依赖会因为物理节点的非“单件”破坏,从本质上讲采用Singleton模式本身会对于应用的水平扩展(Scale Out)形成障碍。
如果工程上要对NLB集群中的Singleton对象继续保持其“单件”,那么可以采用集中保存其属性信息(或State信息)。不仅如此,对于如果需要通过外界修改这些属性,那么还需要在外部增加一个串行的集中属性信息更新机制。
图:NLB环境下的多节点Singleton实现
- 对于跨AppDomain的访问,往往需要把复杂的对象通过串行化后进行传递,但是串行化本身会导致单件特性的破坏。因为等于串行化完成了单件对象的“深拷贝”,所以一定不能对于单件对象声明SerializableAttribute属性。下面是一个错用情形示例——为单件类型增加了SerializableAttribute属性。
using System; using System.IO; using System.Runtime.Serialization.Formatters.Binary; namespace VisionLogic.DesignPattern.Practice ...{ [Serializable] public class Singleton ...{ private static Singleton instance; private static BinaryFormatter formatter = new BinaryFormatter(); private Singleton() ...{ } public static Singleton Instance ...{ get ...{ if (instance == null) ...{ instance = new Singleton(); instance.data = 0; } return instance; } } /**//// <summary> /// 共享的实例数据 /// </summary> private int data; public int Data ...{ get ...{ return data; } set ...{ data = value; } } /**//// <summary> /// 把Singleton 实例通过二进制串行化为字符串 /// </summary> /// <param name="graph"></param> /// <returns></returns> public static string SerializeToString(Singleton graph) ...{ MemoryStream memoryStream = new MemoryStream(); formatter.Serialize(memoryStream, graph); Byte[] arrGraph = memoryStream.ToArray(); return Convert.ToBase64String(arrGraph); } /**//// <summary> /// 通过二进制反串行化从字符串回复出Singleton 实例 /// </summary> /// <param name="serializedGraph"></param> /// <returns></returns> public static Singleton DeserializeFromString(string serializedGraph) ...{ Byte[] arrGraph = Convert.FromBase64String(serializedGraph); MemoryStream memoryStream = new MemoryStream(arrGraph); return (Singleton)formatter.Deserialize(memoryStream); } } } using System; using System.Diagnostics; using VisionLogic.DesignPattern.Practice; namespace VisionLogic.DesignPattern.Practice.Client ...{ class Program ...{ static void Main(string[] args) ...{ Singleton instance1 = Singleton.Instance; string serializedInstance =
Singleton.SerializeToString(instance1); Singleton instance2 =
Singleton.DeserializeFromString(serializedInstance); instance1.Data = 30; Trace.WriteLine(instance2.Data); instance2.Data = 15; Trace.WriteLine(instance1.Data); } } } 结果: 0 30
示例8:错用SerializableAttribute导致的单件特性的丧失
示例结果清楚地表明instance1和instance2其实是不同的实例对象。同样的,如果单件实例对象通过Remoting在服务器和客户端传递,将会把这个非单件的效果在不同的计算节点间传递。