独体模式(Sigleton Pattern)
以下定义与心得采自深入浅出设计模式一书:
独体模式可说是类图最简单的一个设计模式。事实上类图中也只定义了一个类,
我们常常把对象的类定义下来,然后调用构造函数来建立许多对象的实例。
原因是因为类被看待成一个分类,事实上他就像定义一个种族。用来建立同一种类的对象的集合。
因此我们常常建立鸭子类,然后建立许多鸭子,又建立Pizza类,然后生产出许多Pizza。
正是因为生活中,我们必须因应需求(需求决定你分析的角度),去大量制造这些产品或生物(呵)。
然而,回头看看生活周遭,有哪些对象须不需要大量生产,特别它是在操作的时候,只”需要”一个,而且”只能”有一个?
例如说: Thread Pool、Cache、Registry、Login User
上述的例子假如大量的制造就会导致许多问题产生。最明显的就是行为异常、资源使用过量、或者是不一致的结果!
回归先前的对象种族论点。反观我们现行的程序环境,资源其实是非常有限的。
这些宝贵而且稀有的内存、共用的区块、以及程序片段。在存取、操作以及安全上都应该只存在一个实例。
最明显的例子就是程序中的全域变量,全域变量让所有其他程序能够存取到变量的值以及程序之间约定好的对象。
在程序最常见的作法就是透过声明静态变量即可。
只不过,透过全域变量还是有一些缺点:对象一开始就必须建立好,但是假如程序过程中并没有使用到这个对象的话,
就形成了一种资源的浪费!这类的对象被大量地声明其实是非常耗费资源的。
因此!!透过独体模式,可以让对象在被需要的时候才建立对象!来看看吧!
我们通常会采用下列的方式来建立对象
1: public ClassA{ //类A
2: private ClassA(){} //构造函数
3: }
看看一些问题,第一个,我们不能让所有的人都可以随意的来建立对象A的实例!有时不当的开放也是一项程序设计缺点。
为了不让其他人可以随意地建立与多次的实例化对象,至少,我先要把对象的权力揽在自己的手上!(构造函数设为私有)
但这好像有一些不合常理,看出来了吗?
含有私有的构造函数之类好像不能实例化耶
必竟类A的实例,才能调用类A的构造函数,但是没有这样的实例,因为没有其他的类可以实例化这个类A。
这正是“鸡生蛋”、“蛋生鸡”的难题不是吗?
而解决这个问题的方法就是声明一个 “类方法”,而不是透过对象方法去建立实物。
1: public Class ClassA{
2: private ClassA(){}
3: public static createInstance(){
4: return new ClassA();
7: }
8: }
11: //建立对象
12: ClassA.createInstance();
是的,建立静态方法就可以了。这个方法是依附在类上,而不是对象上,因此不用透过实例化对象来调用了。
但是还有一些问题,能够继续修改程序,建立出唯一一个的类A的实例吗?
来看看,以下是一个经典的独体模式的程序:
1: public class Singleton{
2: //透用一个静态变量记录实例
3: private static Singleton uniqueInstance;
4: //构造函数的啦
5: private Singleton(){}
6: public static Singleton getInstance(){
7: if (uniqueInstance == null)
8: uniqueInstance = new Singleton();
9: return uniqueInstance;
10: }
11: }
正常来看,一眼就可以看出这个程序正是为了达成独体模式所完成的程序嘛。
从getInstance中的判断式中可以知道,如果不需要此实例,就不会产生此实例
只有在需要时,才会产生。这边给他一个名词:Lazy Instantiaze(拖延实例化)
这也是独体模式的一个特色。当外人要取得此类的实例时,不再是主动的建构实例!
而是请求得到一个实例!调用了getInstance()这个方法,对象就会立刻现身,随时可以工作。
事实上可能是当下被建立,也有可能是早前被建立出来的。
再来看看这次又有什么需求来挑战独体模式呢?
以下实例是自己想的
假设:大家都知道“古坑乐园”现在都有很多另人心脏狂跳的游乐设施,而且现在这些器材都是透过现代化电脑来控制。
包含设施的启动、安全设备的启动、检查与设施的执行。
看看它们的程序,将发现这些程序必须写的很小心(当然本篇只是简单化),要努力防止不好的事情发生。例如:设施启动的时候,安全设备却还没启用!或是还在运行的时候,安全设备就被解除!(恐怖极了)
1: public class Roller_Coaster
2: {
3: private bool safeGuarded; //启用安全设备!
4: private bool facilityState; //设备运行状态
5:
6: public Roller_Coaster()
7: {
8: safeGuard = false; //安全设备解除,可以让乘客上座与离座
9: facilityRun = false; //运行状态为停止(False)
10: }
11:
12: public void run()
13: {
14: //执行的时候,必须检查安全设备是不是启用
15: //若没有启用,就必须启用,才能执行设施
16: if (!isGuarded()){
17: safeGuarded = true;
18: facilityState = true;
19: }
20: }
21:
22: public void stop()
23: {
24: //在停止的时候,必须确认设施正在执行,而且安全设备是启用的
25: if (isRuning() && isGuarded())
26: {
27: facilityState = false;
28: safeGuarded = false;
29: }
30: }
31:
32: public Boolean isRuning()
33: {
34: return facilityState;
35: }
36:
37: public bool isGuarded()
38: {
39: return safeGuarded;
40: }
41: }
42:
然而,假如上述的云霄飞车万一同时有两个实例,可不可能会发生不好的事情呢?
假如现在列车要运行了,结果改”错”了列车的实例,让安全设备解除,或是未被启用。这个风险是有可能发生的…
因此我们身负重任,来改进古坑乐园的云宵飞车类吧,把这个类改成独体模式!!
1: //唯一的对象
2: private static Roller_Coaster uniqueInstance;
3:
4:
5: //改成私有的
6: private Roller_Coaster()
7: {
8: safeGuard = false; //安全设备解除,可以让乘客上座与离座
9: facilityRun = false; //运行状态为停止(False)
10: }
11:
12: public static Roller_Coaster getInstance(){
13: if (uniqueInstance == null)
14: uniqueInstance = new Roller_Coaster();
15: return uniqueInstance;
16: }
没错,上述程序列出关键的部分。
定义独体模式
经典的独体模式就如上述程序所显示。回头来看看~定义:
定义:独体模式确保一个类只有一个实例,并给它一个存取的全域点(Global Point)。
来自于深入浅出设计模式一书(Headfirst in Design Pattern)
这样的实践方式,也提供了一个方法,让程序在任何地方都可以取得此独体,当需要的时候
请类提供,就可以得到独体对象,这种作法透过Lazy Instantiaze的方式建立独体,这种作法对资源集中对象特别的重要。
透过独体模式,提供和全域变量一样简单的存取方式,但是又比全域变量多了一个优点(Lazy Instantiaze)呢。
类图如下图所示:
然而,潜在的问题有可能发生吧,会发生灾难的风险还是非常显著的……
看来云宵飞车的系统要让我们失望了…是哪里呢?尽管利用了经典的独体实践改善了程序。
但是Roller_Coaster中的Run的方法,竟然允许在启动的过程中,让人解除安全设备,或是一直运行下去,这可是会出人命的啊…
状况是来自于你有可能产生两个设施的对象,导致问题的发生。
深入浅出设计模式一书提到,新的独体模式虽然顺利,一开始只是为了让程序更有执行效率。一旦加入”多线程”的功能后,就有可能出事…这是潜在的问题
为什么呢?以JVM的高度来看,假如程序是以以下的Timing执行,这个程序就有可能产生出两个对象,导置扰乱程序。
你可以看到程序执行时互相重叠的部分。
然而,能改善多线程所产生的潜在问题吗?(必竟我们必须认定所有的程序都是多线程的程序)
1.在Java中可以采用同步化(synchronized)的方法(指同一个方法迫使线程仅有一个能被执行)
虽然这个方法可以解决问题,却会降低效率呢,必竟每次只有第一次执行此方法时,才真正需要同步化。
然后其他每次调用这个方法却仍执行同步化。反正会是一种累赘。
2.使用“率先”建立实例,而不用拖延实例化(Lazy instaniaze)的作法。也就是存取实例前,一定会先建立此实例
这个方式可以改善第一种方法造成的效率下降,而且保证安全。
3双重上锁技巧.(Double-Checked Locking)
以下以古坑乐园的云宵飞车来介绍第三项双重上锁技巧
1: //唯一的对象
2: private volatile static Roller_Coaster uniqueInstance;
3: public static Roller_Coaster getInstance()
4: {
5: if (uniqueInstance == null)
6: {
7: lock (Roller_Coaster.uniqueInstance)
8: {
9: if (uniqueInstance == null)
10: {
11: uniqueInstance = new Roller_Coaster();
12: }
13: }
14: }
15: return uniqueInstance;
16: }
你可以看到,在getInstance的存取点方法的地方,我们建立了两次的检查实例的动作,而第二个检查实例必须Lock住uniqueInstance,确保他在这段时间内不会被修改!
这个是.NET中的作法,移到Java中就是透过Synchorized的关键字!
而另一个改变是第二行的volatile关键字,这个关键字确保,当uniqueInstance变量被实例化的时候,多个线程处理uniqueInstance的作法是正确的。
深入浅出设计模式建议,若性能是你关心的重点,那这个作法可以帮你大大减少getInstance的时间耗费。
以上是独体模式的分享,因为内涵与目的非常的明确,仅简单作为笔记参考。
我们可能还是搞不清楚全域变量与独体模式。
我们再度聚焦此模式的目的:确保类仅有一个实例,并且提供全域存取的方法。然而全域变量可以提供全域的存取,但是不能确保只有一个实例。
后记:在设计程序的时候,想想这些模式能带来什么样的好处,甚至是是否适合被继承?独体模式并不一定适合设计进入程序库中
但适合使用独体模式的机会很多。只要当你需要控制实例个数时,就应当使用独体模式
另外,回文中也有很多独体模式的混合应用,可以参考之^^。
原文:大专栏 独体模式(Singleton Pattern)