一个围棋下得好的人知道,好的"形"对于围棋非常重要。形是棋子在棋盘上的几何形状的抽象化。 形就是模式(Pattern),也是人脑把握和认识外界的关键。而人脑对处理模式的能力也非常高超, 人可以在几百张面孔中一下子辨认出所熟悉的脸来,就是一个例子。
简而言之,在我们处理大量问题时,在很多不同的问题中重复出现的一种性质,它使得我们可以使用一种方法来描述问题实质并用本质上相同,但细节永不会重复的方法去解决,这种性质就叫模式。模式化过程是把问 题抽象化,在忽略掉不重要的细节后,发现问题的一般性本值,并找到普遍使用的方法去解决的过程。
发现模式是与研究模式同时发生的,发现一个新的模式很不容易。一个好的模式必须满足以下几点:
1、它可以解决问题。模式不能仅仅反映问题,而必须对问题提出解决方案。
2、它所提出解决方案是正确的,而且不是很明显的。
3、它必须是涉及软件系统深层的结构的东西,不能仅是对已有的模块的描述。
4、它必须满足人的审美,简洁美观。
换言之,一个美妙的东西不一定就是模式,但是一个模式必须是一个美妙的东西。
软件工程学的各个方面,诸如开发组织,软件处理,项目配置管理,等等,都可以看到模式的影子。但至今 得到了最好的研究的是设计模式和组织模式。在软件编程中使用模式化方法, 是在编程对象化之后才开始得到重视的。软件编程中模式化方法的研究,也是在九十年代才开始。
在面向对象的编程中使用模式化方法研究的开创性著作,是
Design Patterns - Elements of Reusable Object-Oriented Software, E.Gamma, R. Helm, R. Johnson, and J. Vlissides,1995, Addison-Wesley.
这四位作者通常被称为四人帮(Gang of Four, 或GoF)。(在这个词出现以后,很多西方商业炒作利用这个 路人皆知的词赚钱,有一个八十年代的美国四人乐队以此为队名。在英国政界更曾有好几个小帮派被称为四人帮。 在这里大家使用这个词称呼这四个著名作者,带有戏虐成分。)
由于爪哇语言的特点,使得模式在爪哇语言的实现有自己的特点。 爪哇语言是现今最普及的纯粹OOP的编程语言,使用爪哇语言编程的程序师平均的素质也相对比较高。 这些程序师往往不满足于只是实现程序功能要求,他们常常想要在代码结构,编程风格,乃至解决问题的 思考方式上不断进取和自我完善。模式,就是在大量的实践中总结和理论化之后的优选的代码结构,编程风格, 及解决问题的思考方式。对模式的了解和掌握,是爪哇程序师提高自身素质的一个很好的方向。
作者在学习和工作中把自己的体会 总结下来,藉以与读者交流提高。
作者在后面会使用简单的UML(统一建模语言,Unified Modelling Languge)。由于市场上有很多介绍UML 的书,而作者在后面使用到的UML又极为简单,因此只在此作一极为简单的介绍,目的是让没有接触过UML的 读者能看懂后面的讲述。
图1. UML的类图举例 |
在图1的类图中可以看出,表示类的框分成四层:类名,变量清单,函数清单和属性清单。 变量名如是正体字,表明类是实的(Concrete,即可以实例化的类),变量名如是斜体字,表明类是抽象的。 显然,我们在图中给出了一个实的类。
在图1的类ClassUML中,一个变量或函数(方法)左面如果有一个加(+)号,表示它是公开的, 左面如果有一个减(-)号,表示它是私有的,左面如果有一个井(#)号,表示它是保护的。
一个属性即由一个内部变量,一个赋值函数(mutator)和一个取值函数(accessor)组成的结构。
在类的方框的右上角里,通常还分两行写出类的父类和所实现的接口。在后面读者会看到例子。
在类与类之间,会有线条指明它们之间的关系。在类与类之间可以发生推广(与继承相反),依赖,累积和关联 等关系。在后面读者看到例子时作者会加以解释。
package com.javapatterns.singleton.demos; 什么是创立性模式 单态模式的几种实现
图中的关系线表明,此类自已将自己实例化。
值得指出的是,由于构造子是私有的,因此此类不能被继承。 懒汉式单态类
代码清单3.懒汉式单态类。
图中的关系线表明,此类自已将自己实例化。 读者可能会注意到,在上面给出 懒汉式单态类实现里,使用了在多线程编程中常要使用的,著名的双重检查原则。对双重检查原则 和多线程编程要点不十分熟悉的读者,可以看看后面给出的问答题。 同样,由于构造子是私有的,因此此类不能被继承。 饿汉式单态类在自己被加载时就将自己实例化。既便加载器是静态的,在饿汉式单态类被加载时仍 会将自己实例化。单从资源利用效率角度来讲,这是比懒汉式单态类稍差些。从速度和反应时间角度来 讲,则比懒汉式单态类稍好些。然而,懒汉式单态类在实例化时必须处理好在多个线程同时首次引 用此类时,实例化函数内部关键段的访问限制问题。特别是当单态类作为资源控器,在实例化时必然涉及 资源初始化,而资源初始化很有可能耗费时间。这意味着出现多线程同时首次引 用此类的几率变得较大。 饿汉式单态类可以在爪哇语言内实现,但不易在C++内实现,因为静态初始化在C++里没有固定的顺序, 因而静态的m_instance变量的初始化与类的加载顺序没有保证,可能会出问题。这就是为什么GoF在提出 单态类的概念时,举的例子是懒汉式的。他们的书影响之大,以致爪哇语言中单态类的例子也大多是 懒汉式的。实际上,作者认为饿汉式单态类更符合爪哇语言本身的特点 |
登记式单态类
登记式单态类是GoF为了克服饿汉式单态类及懒汉式式单态类均不可继承的缺点而设计的。 作者把他们的例子翻译为爪哇语言,并将它自己实例化的方式从懒汉式改为饿汉式。只是它的 子类实例化的方式只能是懒汉式的,这是无法改变的。
图4. 登记式单态类的一个例子 |
图中的关系线表明,此类自已将自己实例化。
package com.javapatterns.singleton.demos; import java.util.HashMap; public class RegSingleton { protected RegSingleton() {} static public RegSingleton getInstance(String name) { if (name == null) { name = "com.javapatterns.singleton.demos.RegSingleton"; } if (m_registry.get(name) == null) { try { m_registry.put( name, Class.forName(name).newInstance() ) ; } catch(Exception e) { System.out.println("Error happened."); } } return (RegSingleton) (m_registry.get(name) ); } static private HashMap m_registry = new HashMap(); static { RegSingleton x = new RegSingleton(); m_registry.put( x.getClass().getName() , x); } public String about() { return "Hello, I am RegSingleton."; } } |
它的子类
图5. 登记式单态类子类的一个例子。 图中的关系线表明,此类是由父类将自己实例化的。 |
package com.javapatterns.singleton.demos; import java.util.HashMap; public class RegSingletonChild extends RegSingleton { public RegSingletonChild() {} static public RegSingletonChild getInstance() { return (RegSingletonChild) RegSingleton.getInstance( "com.javapatterns.singleton.demos.RegSingletonChild" ); } public String about() { return "Hello, I am RegSingletonChild."; } } |
在GoF原始的例子中,并没有getInstance()方法,这样得到子类必须调用文类的getInstance(String name) 方法,并传入子类的名字,很不方便。 作者在登记式单态类子类的例子里,加入了getInstance()方法,这样做的好处是RegSingletonChild 可以通过这个方法,返还自已的实例,而这样做的缺点是,由于数据类型不同,无法在RegSingleton提供 这样一个方法。
由于子类必须充许父类以构造子调用产生实例,因此它的构造子必须是公开的。这样一来,就等于允许了 以这样方式产生实例而不在父类的登记中。这是登记式单态类的一个缺点。
GoF曾指出,由于父类的实例必须存在才可能有子类的实例,这在有些情况下是一个浪费。 这是登记式单态类的另一个缺点.
爪哇语言里的垃圾回收
爪哇语言里垃圾回收使得单态类的使用变得有点复杂。原因就在于JDK1.1版里加进去的类的自动清除。 这种类的垃圾回收会清除掉类本身,而不仅仅是对象!事实上JDK1.1甚至可以清除掉一些系统类!
在JDK1.0.x版本里,类的自动清除尚未加入。
在JDK1.2及以后的版本里,升阳公司又收紧了类的垃圾回收规则,它规定,所有通过局部的和系统的 类加载器加载的类,永不被回收。并且,通过其它类加载器加载的类,只有在加载器自己被回收后才可被回收。
在1.1版JDK里使用单态类的读者,如果不了解这一版爪哇语言的特点,很有可能会遇到类消失掉的奇特问题。 为了使你的单态类能在所有版本的爪哇环境里使用,作者特别提供一个"看守"类程序,它能保证你的单态类, 甚至其它任何对象,一旦交给"看守"对象,即不会莫名其妙地被垃圾回收器回收,直到你把它从"看守" 那里把它释放出来。
图6. "看守"类的一个例子 |
package com.javapatterns.singleton.demos; import java.util.Vector; /** * This class keeps your objects from garbage collected */ public class ObjectKeeper extends Thread { private ObjectKeeper() { new Thread(this).start(); } public void run() { try { join(); } catch (InterruptedException e) {} } /** * Any object passed here will be kept until you call discardObject() */ public static void keepObject(Object myObject) { System.out.println(" Total number of kept objects: " + m_keptObjects.size()); m_keptObjects.add(myObject); System.out.println(" Total number of kept objects: " + m_keptObjects.size()); } /** * This method will remove the protect of the object you pass in and make it * available for Garbage Collector to collect. */ public static void discardObject(Object myObject) { System.out.println(" Total number of kept objects: " + m_keptObjects.size()); m_keptObjects.remove(myObject); System.out.println(" Total number of kept objects: " + m_keptObjects.size()); } private static ObjectKeeper m_keeper = new ObjectKeeper(); private static Vector m_keptObjects = new Vector(); } |
代码清单6. 看守类的一个实现。
看守类应当自我实例化,而且在每个系统里只需一个实例。这就意味着看守类本身就应当是单态类。当然,类 消失的事情绝不可以发生在它自己身上。作者提供的例子刚好满足所有的要求.
一个实用的例子
这里作者给出一个读取属性(properties)文件的单态类,作为单态类的一个实用的例子。 属性文件如同老式的视窗编程时的.ini文件,属于系统的“资源“,而读取属性文件即为资源管理, 显然应当由一个单态类负责。
图7. 这个例子的UML |
显然,在大多数的系统中都会涉及属性文件的读取问题,因而这个例子非常有实用价值。 在这个例子里,作者假定需要读取的属性文件就在当前目录中,且名为singleton.properties。 在这个文件中有如下的一些属性项:
node1.item1=How node1.item2=are node2.item1=you node2.item2=doing node3.item1=? |
代码清单7. 属性文件内容
本例子的源代码如下:
package com.javapatterns.singleton.demos; import java.util.Properties; import java.io.FileInputStream; import java.io.File; public class ConfigManager { /** * 私有的构造子, 用以保证实例化的唯一性 */ private ConfigManager() { m_file = new File(PFILE); m_lastModifiedTime = m_file.lastModified(); if(m_lastModifiedTime == 0) { System.err.println(PFILE + " file does not exist!"); } m_props = new Properties(); try { m_props.load(new FileInputStream(PFILE)); } catch(Exception e) { e.printStackTrace(); } } /** * * @return 返还ConfigManager类的单一实例 */ synchronized public static ConfigManager getInstance() { return m_instance; } /** * 读取一特定的属性项 * * @param name 属性项的项名 * @param defaultVal 属性项的缺省值 * @return 属性项的值(如此项存在), 缺省值(如此项不存在) */ final public Object getConfigItem(String name, Object defaultVal) { long newTime = m_file.lastModified(); // 检查属性文件是否被其它程序(多数情况是程序员手动)修改过。 // 如果是,重新读取此文件。 if(newTime == 0) { // 属性文件不存在 if(m_lastModifiedTime == 0) { System.err.println(PFILE + " file does not exist!"); } else { System.err.println(PFILE + " file was deleted!!"); } return defaultVal; } else if(newTime > m_lastModifiedTime) { m_props.clear(); // Get rid of the old properties try { m_props.load(new FileInputStream(PFILE)); } catch(Exception e) { e.printStackTrace(); } } m_lastModifiedTime = newTime; Object val = m_props.getProperty(name); if( val == null ) { return defaultVal; } else { return val; } } /** * 属性文件全名 */ private static final String PFILE = System.getProperty("user.dir") + "/Singleton.properties"; /** * 对应于属性文件的文件对象变量 */ private File m_file = null; /** * 属性文件的最后修改日期 */ private long m_lastModifiedTime = 0; /** * 属性文件所对应的属性对象变量 */ private Properties m_props = null; /** * 本类可能存在的唯一的一个实例 */ private static ConfigManager m_instance = new ConfigManager(); } |
显然,作者是用饿汉型实现方法,从而避免了处理多线程访问可能引起的麻烦。在下面的源代码里,作者演示了怎样利用看守类来"看守"和"释放"ConfigMan类,以及怎样调用ConfigMan来读取属性文件。
ObjectKeeper.keepObject(ConfigManager.getInstance()); BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); System.out.println("Type quit to quit"); do { System.out.print("Property item to read: "); String line = reader.readLine(); if(line.equals("quit")) { break; } System.out.println(ConfigManager.getInstance().getConfigItem(line, "Not found.")); } while(true); ObjectKeeper.discardObject(ConfigManager.getInstance()); |
显然,作者是用饿汉型实现方法,从而避免了处理多线程访问可能引起的麻烦。 在下面的源代码里,作者演示了怎样利用看守类来"看守"和"释放"ConfigMan类,以及 怎样调用ConfigMan来读取属性文件。
ObjectKeeper.keepObject(ConfigManager.getInstance()); BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); System.out.println("Type quit to quit"); do { System.out.print("Property item to read: "); String line = reader.readLine(); if(line.equals("quit")) { break; } System.out.println(ConfigManager.getInstance().getConfigItem (line, "Not found.")); } while(true); ObjectKeeper.discardObject(ConfigManager.getInstance()); |
下面的图显示出上面代码运行时的情况。
问答题
代码清单10.懒汉式单态类的变种。 3、在LazySingleton的例子中,出现了两层检查 if (m_instance == null)。这是否必要?如果将内层的检查去掉,会出问题吗? 4、同上,如果将外层的检查去掉,会出问题吗? 5、举例说明如何调用EagerSingleton类。 6、举例说明如何调用RegSingleton类和RegSingletonChild类。 7、在看守类中,变量m_keptObjects还可选择什么数据类型,使得程序占用更小? 8、设法用实例生成的时间,实例的identityHashCode,类加载器,实例的总数,实例化的序数 来确定一个单态确实是单态。 问答题答案 1、一个变量不能自已初始化,不可能有继承的关系。在爪哇语言里并没有真正的"全程"变量, 一个变量必须属于某一个类。而在复杂的程序当中,一个静态变量的 初始化发生在哪里,常常是一个不易确定的问题。当然,使用变量并没有什么错误,就好选择使用Fortran 语言而非爪哇语言编程并不是一种对错的问题一样。 2、这样做不会出错,但是效率不好。在原来的源代码中,synchronized行为只在第一次调用 此方法起作用,以后的调用均不会遇到。而在这里,任何凋用都会遇到synchronized的限制,这无异于 人为制造一个不必要的独木桥,十分愚蠢。 3、这样做一定会出问题。在第一次调用getInstance()时可能有多个线程几乎同时到达, 只有一个线程能到达内层检 查之内,其它的线程会在synchronized()语句处等待。这样当第一线程完成实例化之后,等待在 synchronized()语句处的其它线程会逐一获准进入synchronized()之后的语句。如果那里没有第二 次检查,它们就会逐一试图进行实例化,而这是错的。 4、这样不会出问题,但是效率不好,十分愚蠢。道理与第一题类似。 5、
6、见上题答案。 7、变量m_keptObjects还可选择HashMap,这样更省资源 |