什麼是模式
一個圍棋下得好的人知道,好的"形"對於圍棋非常重要。形是棋子在棋盤上的幾何形狀的抽象化。形就是模式(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)。(在這個詞出現以後,很多西方商業炒作利用這個 路人皆知的詞賺錢,有一個八十年代的美國四人樂隊以此為隊名。在英國政界更曾有好幾個小幫派被稱為四人幫。在這裡大家使用這個詞稱呼這四個著名作者,帶有戲虐成分。)
由於Java語言的特點,使得模式在Java語言的實現有自己的特點。Java語言是現今最普及的純粹OOP的編程語言,使用Java語言編程的程序師平均的素質也相對比較高。這些程序師往往不滿足於只是實現程序功能要求,他們常常想要在代碼結構,編程風格,乃至解決問題的 思考方式上不斷進取和自我完善。模式,就是在大量的實踐中總結和理論化之後的優選的代碼結構,編程風格, 及解決問題的思考方式。對模式的了解和掌握,是Java程序師提高自身素質的一個很好的方向。
作者在學習和工作中把自己的體會 總結下來,藉以與讀者交流提高。
作者在後面會使用簡單的UML(統一建模語言,Unified Modelling Languge)。由於市場上有很多介紹UML 的書,而作者在後面使用到的UML又極為簡單,因此只在此作一極為簡單的介紹,目的是讓沒有接觸過UML的 讀者能看懂後面的講述。
圖1. UML的類圖舉例
在圖1的類圖中可以看出,表示類的框分成四層:類名,變量清單,函數清單和屬性清單。變量名如是正體字,表明類是實的(Concrete,即可以實例化的類),變量名如是斜體字,表明類是抽象的。顯然,我們在圖中給出了一個實的類。
在圖1的類ClassUML中,一個變量或函數(方法)左面如果有一個加(+)號,表示它是公開的, 左面如果有一個減(-)號,表示它是私有的,左面如果有一個井(#)號,表示它是保護的。
一個屬性即由一個內部變量,一個賦值函數(mutator)和一個取值函數(accessor)組成的結構。
在類的方框的右上角裡,通常還分兩行寫出類的父類和所實現的接口。在後面讀者會看到例子。
在類與類之間,會有線條指明它們之間的關系。在類與類之間可以發生推廣(與繼承相反),依賴,累積和關聯 等關系。在後面讀者看到例子時作者會加以解釋。
package com.javapatterns.singleton.demos;
public class ClassUML {
public ClassUML() {}
private void aPrivateFunction() {}
public void aPublicMethod() {}
public int getAProperty(){ return aPrivateVar; }
public void setAProperty(int aPrivateVar)
{ this.aPrivateVar = aPrivateVar; }
static public void aStaticMethod() {}
protected void aProtectedMethod() {}
private int aPrivateVar;
public int aPublicVar;
protected int aProtectedVar;
}
代碼清單1. ClassUML類的源代碼。
什麼是創立性模式
創立性模式(Creational Patterns)是類在實例化時使用的模式。當一些系統在創立對象時,需要動態地決定 怎樣創立對象,創立哪些對象。創立性模式告訴我們怎樣構造和包裝這些動態的決定。創立性模式通常包括 以下的模式
1、工廠函數模式
2、抽象工廠類模式
3、建設者模式
4、原始模型模式
5、單態模式
單態模式
一個單態類只可有一個實例。這樣的類常用來進行資源管理。
需要管理的資源包括軟件外部資源,譬如,每台 計算機可以有若干個打印機,但只能有一個打印處理器軟件。每台計算機可以有若干傳真卡,但是 只應該有一個傳真軟件管理傳真。每台計算機可以有若干通訊端口,你的軟件應當集中管理這些 通訊端口,以避免同時一個通訊端口被兩個請求同時調用。
需要管理的資源包括軟件內部資源,譬如,大多數的軟件都有一個(甚至多個)屬性(properties)文件 存放系統配置。這樣的系統應當有一個對象來管理一個屬性文件。很多軟件都有數據庫,一般而言, 整個軟件應當使用一個聯接通道,而不是任意在需要時就新打開一個聯接通道。
需要管理的軟件內部資源也包括譬如負責紀錄網站來訪人數的部件,記錄軟件系統內部事件、出錯 信息的部件,或是進行系統表現監查的的部件,等等。這些部件都必須集中管理,不可政出多頭。
單態類的特性
綜合而言,
1、單態類只可有一個實例。
2、它必須自己創立自己這唯一的一個實例。
3、它必須給所有其它的類提供自己這一實例。
最後,單態類在理論和實踐上都並非限定只能有"一個"實例,而是很容易推廣到任意有限個實例的情況。
單態模式的幾種實現
由於Java語言的特點,使得單態模式在Java語言的實現有自己的特點。這些特點主要表現在怎樣實例化上。
餓漢式單態類
餓漢式單態類是在Java語言裡實現得最為簡便的單態類。
圖2.餓漢式單態類的UML類圖
圖中的關系線表明,此類自已將自己實例化。
package com.javapatterns.singleton.demos;
public class EagerSingleton {
private EagerSingleton() { }
public static EagerSingleton getInstance() {
return m_instance;
}
private static final EagerSingleton m_instance = new EagerSingleton();
}
代碼清單2.餓漢式單態類。
值得指出的是,由於構造子是私有的,因此此類不能被繼承。
懶漢式單態類
懶漢式單態類在第一次被引用時將自己實例化。如果加載器是靜態的,那麼在懶漢式單態類被加載時不 會將自己實例化。
package com.javapatterns.singleton.demos;
public class LazySingleton {
private LazySingleton() { }
public static LazySingleton getInstance()
{
if (m_instance == null)
{
file://More than one threads might be here!!!
synchronized(LazySingleton.class)
{
if (m_instance == null)
{
m_instance = new LazySingleton();
}
}
}
return m_instance;
}
private static LazySingleton m_instance = null;
}
代碼清單3.懶漢式單態類。
圖3.懶漢式單態類
圖中的關系線表明,此類自已將自己實例化。
讀者可能會注意到,在上面給出 懶漢式單態類實現裡,使用了在多線程編程中常要使用的,著名的雙重檢查原則。對雙重檢查原則 和多線程編程要點不十分熟悉的讀者,可以看看後面給出的問答題。
同樣,由於構造子是私有的,因此此類不能被繼承。
餓漢式單態類在自己被加載時就將自己實例化。既便加載器是靜態的,在餓漢式單態類被加載時仍 會將自己實例化。單從資源利用效率角度來講,這是比懶漢式單態類稍差些。從速度和反應時間角度來 講,則比懶漢式單態類稍好些。然而,懶漢式單態類在實例化時必須處理好在多個線程同時首次引 用此類時,實例化函數內部關鍵段的訪問限制問題。特別是當單態類作為資源控器,在實例化時必然涉及 資源初始化,而資源初始化很有可能耗費時間。這意味著出現多線程同時首次引 用此類的幾率變得較大。
餓漢式單態類可以在Java語言內實現,但不易在C++內實現,因為靜態初始化在C++裡沒有固定的順序, 因而靜態的m_instance變量的初始化與類的加載順序沒有保證,可能會出問題。這就是為什麼GoF在提出 單態類的概念時,舉的例子是懶漢式的。他們的書影響之大,以致Java語言中單態類的例子也大多是 懶漢式的。實際上,作者認為餓漢式單態類更符合Java語言本身的特點。
登記式單態類
登記式單態類是GoF為了克服餓漢式單態類及懶漢式式單態類均不可繼承的缺點而設計的。作者把他們的例子翻譯為Java語言,並將它自己實例化的方式從懶漢式改為餓漢式。只是它的 子類實例化的方式只能是懶漢式的,這是無法改變的。
圖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.";
}
}
代碼清單4. 登記式單態類。(注意為簡單起見,這裡沒有考慮多線程訪問限制的問題,讀者可自行加入一個有雙重 檢查的訪問限制)
它的子類
圖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.";
}
}
代碼清單5. 登記式單態類的子類。
在GoF原始的例子中,並沒有getInstance()方法,這樣得到子類必須調用文類的getInstance(String name) 方法,並傳入子類的名字,很不方便。作者在登記式單態類子類的例子裡,加入了getInstance()方法,這樣做的好處是RegSingletonChild 可以通過這個方法,返還自已的實例,而這樣做的缺點是,由於數據類型不同,無法在RegSingleton提供 這樣一個方法。
由於子類必須充許父類以構造子調用產生實例,因此它的構造子必須是公開的。這樣一來,就等於允許了 以這樣方式產生實例而不在父類的登記中。這是登記式單態類的一個缺點。
GoF曾指出,由於父類的實例必須存在才可能有子類的實例,這在有些情況下是一個浪費。這是登記式單態類的另一個缺點。
Java語言裡的垃圾回收
Java語言裡垃圾回收使得單態類的使用變得有點復雜。原因就在於JDK1.1版裡加進去的類的自動清除。這種類的垃圾回收會清除掉類本身,而不僅僅是對象!事實上JDK1.1甚至可以清除掉一些系統類!
在JDK1.0.x版本裡,類的自動清除尚未加入。
在JDK1.2及以後的版本裡,升陽公司又收緊了類的垃圾回收規則,它規定,所有通過局部的和系統的 類加載器加載的類,永不被回收。並且,通過其它類加載器加載的類,只有在加載器自己被回收後才可被回收。
在1.1版JDK裡使用單態類的讀者,如果不了解這一版Java語言的特點,很有可能會遇到類消失掉的奇特問題。為了使你的單態類能在所有版本的Java環境裡使用,作者特別提供一個"看守"類程序,它能保證你的單態類, 甚至其它任何對象,一旦交給"看守"對象,即不會莫名其妙地被垃圾回收器回收,直到你把它從"看守" 那裡把它釋放出來。
圖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();
}
代碼清單8. ConfigMan的源代碼。
顯然,作者是用餓漢型實現方法,從而避免了處理多線程訪問可能引起的麻煩。在下面的源代碼裡,作者演示了怎樣利用看守類來"看守"和"釋放"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());
代碼清單8. ConfigMan的源代碼。
顯然,作者是用餓漢型實現方法,從而避免了處理多線程訪問可能引起的麻煩。在下面的源代碼裡,作者演示了怎樣利用看守類來"看守"和"釋放"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());
代碼清單9. 怎樣調用ConfigMan類以讀取屬性文件,及調用看守類來看守和釋放ConfigMan類
下面的圖顯示出上面代碼運行時的情況。
圖8. 代碼運行時的情況
問答題
1、為什麼不使用一個靜態的"全程"變量,而要建一個類?一個靜態的變量當然只能有一個值, 從而自然而然不就是"單態"的嗎?
2、在LazySingleton的例子中,如果把限制訪問的關鍵詞從LazySingleton.class移到getInstance() 方法的聲明語句中,會怎樣?
package com.javapatterns.singleton.demos;
public class LazySingleton {
private LazySingleton() { }
synchronized static public LazySingleton getInstance()
{
if (m_instance == null)
{
m_instance = new LazySingleton();
}
return m_instance;
}
private static LazySingleton m_instance = null;
}
代碼清單10.懶漢式單態類的變種。
3、在LazySingleton的例子中,出現了兩層檢查 if (m_instance == null)。這是否必要?如果將內層的檢查去掉,會出問題嗎?
4、同上,如果將外層的檢查去掉,會出問題嗎?
5、舉例說明如何調用EagerSingleton類。
6、舉例說明如何調用RegSingleton類和RegSingletonChild類。
7、在看守類中,變量m_keptObjects還可選擇什麼數據類型,使得程序占用更小?
8、設法用實例生成的時間,實例的identityHashCode,類加載器,實例的總數,實例化的序數 來確定一個單態確實是單態。
問答題答案
1、一個變量不能自已初始化,不可能有繼承的關系。在Java語言裡並沒有真正的"全程"變量, 一個變量必須屬於某一個類。而在復雜的程序當中,一個靜態變量的 初始化發生在哪裡,常常是一個不易確定的問題。當然,使用變量並沒有什麼錯誤,就好選擇使用Fortran 語言而非Java語言編程並不是一種對錯的問題一樣。
2、這樣做不會出錯,但是效率不好。在原來的源代碼中,synchronized行為只在第一次調用 此方法起作用,以後的調用均不會遇到。而在這裡,任何凋用都會遇到synchronized的限制,這無異於 人為制造一個不必要的獨木橋,十分愚蠢。
3、這樣做一定會出問題。在第一次調用getInstance()時可能有多個線程幾乎同時到達, 只有一個線程能到達內層檢 查之內,其它的線程會在synchronized()語句處等待。這樣當第一線程完成實例化之後,等待在 synchronized()語句處的其它線程會逐一獲准進入synchronized()之後的語句。如果那裡沒有第二 次檢查,它們就會逐一試圖進行實例化,而這是錯的。
4、這樣不會出問題,但是效率不好,十分愚蠢。道理與第一題類似。
5、package com.javapatterns.singleton.demos;
public class RegSingletonTest
{
public static void main(String[] args)
{
file://(1) Test eager
System.out.println( EagerSingleton.getInstance() );
file://(2) Test reg
System.out.println(
RegSingleton.getInstance(
"com.javapatterns.singleton.demos.RegSingleton").about() ) ;
System.out.println( RegSingleton.getInstance(null).about() ) ;
System.out.println(
RegSingleton.getInstance(
"com.javapatterns.singleton.demos.RegSingletonChild").about() ) ;
System.out.println( RegSingletonChild.getInstance().about()) ;
}
}
代碼清單11. 幾種單態類的使用方法。
6、見上題答案。
7、變量m_keptObjects還可選擇HashMap,這樣更省資源。