单例模式
单例模式之所以这么叫,是因为它限制一个类只能有一个实例化对象。经典的实现方式是,创建一个类,这个类包含一个方法,这个方法在没有对象存在的情况下,将会创建一个新的实例对象。如果对象存在,这个方法只是返回这个对象的引用。
单例和静态类不同,因为我们可以退出单例的初始化时间。通常这样做是因为,在初始化的时候需要一些额外的信息,而这些信息在声明的时候无法得知。对于并不知晓对单例模式引用的代码来讲,单例模式没有为它们提供一种方式可以简单的获取单例模式。这是因为,单例模式既不返回对象也不返回类,它只返回一种结构。可以类比闭包中的变量不是闭包-提供闭包的函数域是闭包(绕进去了)。
在JavaScript语言中, 单例服务作为一个从全局空间的代码实现中隔离出来共享的资源空间是为了提供一个单独的函数访问指针。
我们能像这样实现一个单例:
var mySingleton = (function () { // Instance stores a reference to the Singleton var instance; function init() { // 单例 // 私有方法和变量 function privateMethod(){ console.log( "I am private" ); } var privateVariable = "Im also private"; var privateRandomNumber = Math.random(); return { // 共有方法和变量 publicMethod: function () { console.log( "The public can see me!" ); }, publicProperty: "I am also public", getRandomNumber: function() { return privateRandomNumber; } }; }; return { // 如果存在获取此单例实例,如果不存在创建一个单例实例 getInstance: function () { if ( !instance ) { instance = init(); } return instance; } }; })(); var myBadSingleton = (function () { // 存储单例实例的引用 var instance; function init() { // 单例 var privateRandomNumber = Math.random(); return { getRandomNumber: function() { return privateRandomNumber; } }; }; return { // 总是创建一个新的实例 getInstance: function () { instance = init(); return instance; } }; })(); // 使用: var singleA = mySingleton.getInstance(); var singleB = mySingleton.getInstance(); console.log( singleA.getRandomNumber() === singleB.getRandomNumber() ); // true var badSingleA = myBadSingleton.getInstance(); var badSingleB = myBadSingleton.getInstance(); console.log( badSingleA.getRandomNumber() !== badSingleB.getRandomNumber() ); // true
创建一个全局访问的单例实例 (通常通过 MySingleton.getInstance()) 因为我们不能(至少在静态语言中) 直接调用 new MySingleton() 创建实例. 这在JavaScript语言中是不可能的.
在四人帮(GoF)的书里面,单例模式的应用描述如下:
- 每个类只有一个实例,这个实例必须通过一个广为人知的接口,来被客户访问。
- 子类如果要扩展这个唯一的实例,客户可以不用修改代码就能使用这个扩展后的实例。
关于第二点,可以参考如下的实例,我们需要这样编码:
mySingleton.getInstance = function(){ if ( this._instance == null ) { if ( isFoo() ) { this._instance = new FooSingleton(); } else { this._instance = new BasicSingleton(); } } return this._instance; };
在这里,getInstance 有点类似于工厂方法,我们不需要去更新每个访问单例的代码。FooSingleton可以是BasicSinglton的子类,并且实现了相同的接口。
为什么对于单例模式来讲,延迟执行执行这么重要?
在c++代码中,单例模式将不可预知的动态初始化顺序问题隔离掉,将控制权返回给程序员。
区分类的静态实例和单例模式很重要:尽管单例模式可以被实现成一个静态实例,但是单例可以懒构造,在真正用到之前,单例模式不需要分配资源或者内存。
如果我们有个静态对象可以被直接初始化,我们需要保证代码总是以同样的顺序执行(例如 汽车需要轮胎先初始化)当你有很多源文件的时候,这种方式没有可扩展性。
单例模式和静态对象都很有用,但是不能滥用-同样的我们也不能滥用其它模式。
在实践中,当一个对象需要和另外的对象进行跨系统协作的时候,单例模式很有用。下面是一个单例模式在这种情况下使用的例子:
var SingletonTester = (function () { // options: an object containing configuration options for the singleton // e.g var options = { name: "test", pointX: 5}; function Singleton( options ) { // set options to the options supplied // or an empty object if none are provided options = options || {}; // set some properties for our singleton this.name = "SingletonTester"; this.pointX = options.pointX || 6; this.pointY = options.pointY || ; } // our instance holder var instance; // an emulation of static variables and methods var _static = { name: "SingletonTester", // Method for getting an instance. It returns // a singleton instance of a singleton object getInstance: function( options ) { if( instance === undefined ) { instance = new Singleton( options ); } return instance; } }; return _static; })(); var singletonTest = SingletonTester.getInstance({ pointX: 5 }); // Log the output of pointX just to verify it is correct // Outputs: 5 console.log( singletonTest.pointX );
尽管单例模式有着合理的使用需求,但是通常当我们发现自己需要在javascript使用它的时候,这是一种信号,表明我们可能需要去重新评估自己的设计。
这通常表明系统中的模块要么紧耦合要么逻辑过于分散在代码库的多个部分。单例模式更难测试,因为可能有多种多样的问题出现,例如隐藏的依赖关系,很难去创建多个实例,很难清理依赖关系,等等。
要想进一步了解关于单例的信息,可以读读Miller Medeiros 推荐的这篇非常棒的关于单例模式以及单例模式各种各样问题的文章,也可以看看这篇文章的评论,这些评论讨论了单例模式是怎样增加了模块间的紧耦合。我很乐意去支持这些推荐,因为这两篇文章提出了很多关于单例模式重要的观点,而这些观点是很值得重视的。