從Java類庫看設計模式(2)

在上一部分的內容中,我們講到什麼是模式,什麼是設計模式,以及對一個設計模式Observer的詳細闡敘。相信大家對於模式的概念應該是比較的理解了。這部分及以後的內容,將會步入正題,從Java類庫的分析入手,來闡敘設計模式是如何應用到一個完美的設計中的。實際上,Java類庫非常的龐雜,這兒不可能把所有能夠找到的設計模式的例子一一列舉,只是找了一些容易發現的例子。實際上也沒有必要,因爲只要對一個設計模式有足夠的理解,對於它的具體應用而言,倒是一件不是很困難的事情。

Command模式

在設計一般用途的軟件的時候,在C或者C++語言中,用的很多的一個技巧就是回調函數(Callback),所謂的回調函數,意指先在系統的某個地方對函數進行註冊,讓系統知道這個函數的存在,然後在以後,當某個事件發生時,再調用這個函數對事件進行響應。在C或者C++中,實現的回調函數方法是使用函數指針。但是在Java中,並不支持指針,因而就有了Command模式,這一回調機制的面向對象版本。

Command模式用來封裝一個命令/請求,簡單的說,一個Command對象中包含了待執行的一個動作(語句)序列,以執行特定的任務。當然,並不是隨便怎麼樣的語句序列都可以構成一個Command對象的,按照Command模式的設計,Command對象和它的調用者Incvoker之間應該具有接口約定的。也就是說,Invoker得到Command對象的引用,並調用其中定義好的方法,而當Command對象改變(或者是對象本身代碼改變,或者乾脆完全另外的一個Command對象)之後,Invoker中的代碼可以不用更改。這樣,通過封裝請求,可以把任務和任務的實現加以分離。

圖二:Command模式的類圖
圖二:Command模式的類圖 

而對於請求的處理又有兩種不同的方法,一種是Command只充當代理,將請求轉發給某個接受者對象,還有一種是Command對象自己處理完所有的請求操作。當然,這只是兩個極端,更多的情況是Command完成一部分的工作,而另外的一部分這則交給接受者對象來處理。

在新的JDK的代理事件模型中,就可以看作是這樣的一個Command模式。在那個模型中,一個事件監聽者類EventListener監聽某個事件,並根據接口定義,實現特定的操作。比如,當用Document對象的addDocumentListener(DocumentListener listener) 方法註冊了一個DocumentListener後,以後如果在Document對象中發生文本插入的事件,DocumentListener中實現的insertUpdate(DocumentEvent e)方法就會被調用,如果發生文本刪除事件,removeUpdate(DocumentEvent e)方法就會被調用。怎麼樣,想想看,這是不是一個Command模式的應用呢?

然而,最經典的Command模式的應用,莫過於Swing中的Action接口。Action實際上繼承的是ActionListener,也就是說,它也是一個事件監聽者(EventListener)。但是Action作爲一種ActionListener的擴展機制,提供了更多的功能。它可以在其中包含對這個Action動作的一個或者多個文字的或圖標的描敘,它提供了Enable/Disable的功能許可性標誌。並且,一個Action對象可以被多個Invoker,比如實現相同功能的按鈕,菜單,快捷方式所共享。而這些Invoker都知道如何加入一個Action,並充分利用它所提供的擴展機制。可以說,在這兒Action更像一個對象了,因爲它不僅僅提供了對方法的實現,更提供了對方法的描敘和控制。可以方便的描敘任何的事務,這更是面向對象方法的威力所在。

下面我們看一個Command模式的應用的例子。假設要實現這樣的一個任務:Task Schedule。也就是說,我想對多個任務進行安排,比如掃描磁盤,我希望它每1個小時進行一次,而備份數據,我希望它半個小時進行一次,等等等等。但是,我並不希望作爲TaskSchedule的類知道各個任務的細節內容,TaskSchedule應該只是知道Task本身,而對具體的實現任務的細節並不理會。因而在這兒,我們就需要對TaskSchedule和Task進行解耦,將任務和具體的實現分離出來,這不正是Command模式的用武之地嗎?

 


圖三:Command模式的應用例子
圖三:Command模式的應用例子 

 

//抽象的Task接口,作爲回調的Command模式的主體
public interface Task {
  public void taskPerform();
}
//具體的實現了Task接口的子類,實現特定的操作。
public class BackupTask implements Task{
  public void taskPerform(){
    System.out.println("Backup Task has been performed");
  }
}
//具體的實現了Task接口的子類,實現特定的操作。
public class ScanDiskTask implements Task{
  public void taskPerform(){
    System.out.println("ScanDisk Task has been performed");
  }
}
//一個封裝了Task的一個封裝類,提供了一些與Task相關的內容,也可以把這些內容
//這兒不過爲了突出Command模式而把它單另出來,實際上可以和Task合併。
public class TaskEntry {
  private Task task;
  private long timeInterval;
  private long timeLastDone;
  public Task getTask() {
    return task;
  }
  public void setTask(Task task) {
    this.task = task;
  }
  public void setTimeInterval(long timeInterval) {
    this.timeInterval = timeInterval;
  }
  public long getTimeInterval() {
    return timeInterval;
  }
  public long getTimeLastDone() {
    return timeLastDone;
  }
  public void setTimeLastDone(long timeLastDone) {
    this.timeLastDone = timeLastDone;
  }
  public TaskEntry(Task task,long timeInteral){
    this.task=task;
    this.timeInterval =timeInteral;
  }
}
//調度管理Task的類,繼承Thread只是爲了調用其sleep()方法,
//實際上,如果真的作Task調度的話,每個Task顯然應該用單獨的Thread來實現。
public class TaskSchedule extends java.lang.Thread {
  private java.util.Vector taskList=new java.util.Vector();
  private long sleeptime=10000000000l;//最短睡眠時間
  public void addTask(TaskEntry taskEntry){
    taskList.add(taskEntry);
    taskEntry.setTimeLastDone(System.currentTimeMillis());
    if (sleeptime>taskEntry.getTimeInterval()) 
    sleeptime=taskEntry.getTimeInterval();
  }
  //執行任務調度
  public void schedulePermorm(){
    try{
      sleep(sleeptime);
      Enumeration e = taskList.elements();
      while (e.hasMoreElements()) {
        TaskEntry te = (TaskEntry) e.nextElement();
        if (te.getTimeInterval() + te.getTimeLastDone() < 
                System.currentTimeMillis()) {
          te.getTask().taskPerform();
          te.setTimeLastDone(System.currentTimeMillis());
          }
      }
    }catch (Exception e1){
      e1.printStackTrace();
    }
  }
  public static void main (String args[]){
    TaskSchedule schedule=new TaskSchedule();
    TaskEntry taks1=new TaskEntry(new ScanDiskTask(),10000);
    TaskEntry taks2=new TaskEntry(new BackupTask(),3000);
    schedule.addTask(taks1);
    schedule.addTask(taks2);
    while (true){
        schedule.schedulePermorm();
      }
  }
}

 

 

 

程序本身其實沒有多大的意義,因而,程序在編碼的時候也只是用的最簡單的方法來實現的,如果要做一個真正的TaskSchedule的話,這個程序除了結構上的,其它沒有什麼好值得參考的了。

 




回頁首


AbstractFactory 和 FactoryMethod

基本上來說,AbstractFacotry模式和FactoryMethod模式所作的事情是一樣的,都是用來創建與具體程序代碼無關的對象,只是面對的對象層次不一樣,AbstractFactory創建一系列的對象組,這些對象彼此相關。而FactoryMethod往往只是創建單個的對象。

再開始這兩個模式之前,有必要先陳敘一個在設計模式,或者說在整個面向對象設計領域所遵循的一個設計原則:針對接口編程,而不是針對具體的實現。這個思想可以說是設計模式的基石之一。現在的很多對象模型,比如EJB,COM+等等,無不是遵照這個基本原則來設計的。針對接口編程的好處有很多,通過接口來定義對象的抽象功能,方便實現多態和繼承;通過接口來指定對象調用之間的契約,有助於協調對象之間的關係;通過接口來劃分對象的職責,有助於尋找對象,等等。

AbstractFactory和FactoryMethod,還有其他的一些創建型的設計模式,都是爲了實現這個目的而設計出來的。它們創建一個個符合接口規範的對象/對象組,使得用同一個Factory創建出來的對象/對象組可以相互替換。這種可替換性就稱爲多態,是面向對象的核心思想之一。而多態,是通過動態綁定來實現的。


圖四:AbstractFactory模式的類圖
圖四:AbstractFactory模式的類圖 

客戶程序使用具體的AbstractFacotry對象(ConcreteFactoryX)調用CreateProductX()方法,生成具體的ConcreteProductX。每個AbstractFactory所能生成的對象,組成一個系列的對象組,他們可能是相互相關的,緊耦合的。應爲各個AbstractFactory對象所能夠生成的對象組都遵循一組相同的接口(AbstractProductX),因而當程序是針對接口進行編程的時候,這些實現方法各不相同的對象組卻可以相互的替換。

實際上,客戶程序本身並不關心,也不知道具體使用的是那些產品對象。它甚至能夠不理會到底是哪個AbstractFactory對象被創建。在這種情況下,你可能會問,那麼一個AbstractFactory又該如何生成呢? 這時候,就該用該FactoryMethod模式了。

前面有說過,AbstractFactory着重於創建一系列相關的對象,而這些對象與具體的AbstractFactory相關。而FactoryMethod則着重於創建單個的對象,這個對象決定於一個參數或者一個外部的環境變量的值;或者,在一個抽象類中定義一個抽象的工廠方法(也成爲虛擬構造器),然後再實現的子類中返回具體的產品對象。

FactoryMethod可以藉助一個參數或者一個外部的標誌來判斷該具體生成的哪一個子類的實例。比如對於不同的具體情況,需要有不同的AbstractFactory來生成相應的對象組。這時候,FactoryMethod通常作爲一個AbstractFactory對象的靜態方法出現,使得其能夠在具體的對象被創建之前就能夠被調用。

在JAVA中,應用這兩個模式的地方實在太多,下面我們來看一個在JAXP中這兩個模式的應用。JAXP是用來處理XML文檔的一個API。我們都知道XML文件的一個特點就是其平臺無關,流通性能好。因而往往也需要處理他們的程序具有更好的平臺無關性。Java語言是一個比較好的平臺無關語言,可以作爲一個選擇,但是對XML進行解析的解析器確有很多。有時候需要在不同的解析器之間進行切換,這時候,JAXP的良好設計就能夠體現出來了。它能夠允許在不同解析器之間竟進行切換的時候,不用更改程序的代碼。

我們就拿JAXP中的DOM解析器來作爲例子,來例示AbstractFactory和FactoryMethod的用法。


圖五:DOM中工廠模式的應用
圖五:DOM中工廠模式的應用 

上圖中爲了方便起見,只畫出了抽象類和接口,DocumentBuilderFactory和DocumentBuilder都是抽象類。

DocumentBuilderFactory的靜態方法newInstance()方法根據一個外部的環境變量javax.xml.parsers.DocumentBuilderFactory的值來確定具體生成DocumentBuilderFactory的哪一個子類。這兒的newInstance()是一個工廠方法。當DocumentBuilderFactory被創建後,可以調用其newDocumentBuilder()來創建具體一個DocumentBuilder的子類。然後再由DocumentBuilder來生成Document等DOM對象。

下面是創建一個DOM對象的代碼片段:

 

        //第一步:創建一個DocumentBuilderFactory。
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        //第二步:創建一個DocumentBuilder
        DocumentBuilder db = dbf.newDocumentBuilder();
        //第三步:解析XML文件得到一個Document對象
        Document doc = db.parse(new File(filename));

 在這兒,DocumentBuilder,Document,Node等等對象所組成的一個產品組,是和具體的DocumentBuilderFactory相關的。這也就是AbstractFactory模式的含義所在。

 

當然,FactoryMethod模式應用的很廣。這是一個具體的例子,但他不應該限制我們的思路,FactoryMethod和AbstractFactory是解決面向對象設計中一個基本原則--面向接口編程的主要方法。

 




回頁首


Singleton模式

Singleton模式要解決的是對象的唯一性問題。由Singleton模式創建的對象在整個的應用程序的範圍內,只允許有一個對象的實例存在。這樣的情況在Java程序設計的過程中其實並不少見,比如處理JDBC請求的連接池(Connection Pool),再比如一個全局的註冊表(Register),等等,這都需要使用到Singleton,單件模式。

在Java中,最簡單的實現Singleton模式的方法是使用static修飾符,static可以用在內部類上,也可以用在方法和屬性上,當一個類需要被創建成Singleton時,可以把它所有的成員都定義成static,然後再用final和private來修飾其構造函數,使其不能夠被創建和重載。這在程序語法上保證了只會有一個對象實例被創建。比如java.util.Math就是這樣的一個類。

而Singleton模式所作的顯然要比上面介紹的解決方法要複雜一些,也更爲安全一些。它基本的思路也還是使用static變量,但是它用一個類來封裝這個static變量,並攔截對象創建方法,保證只有一個對象實例被創建,這兒的關鍵在於使用一個private或者protected的構造函數,而且你必須提供這樣的一個構造函數,否則編譯器會自動的爲你創建一個public的構造函數,這就達不到我們想要的目的了。

 

public class Singleton {
   //保存唯一實例的static變量
   static private Singleton _instance = null;
/*爲了防止對象被創建,可以爲構造函數加上private修飾符,但是這同樣也防止了子類的對象被創建,因而,可以選用protected修飾符來替代private。*/
   protected Singleton() {
     // ...
   }
    //static方法用來創建/訪問唯一的對象實例,這兒可以對對象的創建進行控制,使得可//以很容易的實現只允許指定個數的對象存在的泛化的Singleton模式。
   static public Singleton instance() {
      if(null == _instance) {
         _instance = new Singleton();
      }
      return _instance;
   }
//  ...
}

 

 

 

對象創建的方法,除了使用構造函數之外,還可以使用Object對象的clone()方法,因而在Singleton中也要注意這一點。如果Singleton類直接繼承於Object,因爲繼承於Object的clone()方法仍保留有其protected修飾,因而不能夠被其他外部類所調用,所以可以不用管它,但是如果Singleton繼承於一個其他的類,而這個類又有重載clone()方法,這時就需要在Singleton中再重載clone()方法,並在其中拋出CloneNotSupportedException,這樣就可以避免多個Singleton的實例被創建了。

在JDK1.2以前的版本中使用Singleton模式的時候有一些需要額外注意的地方,因爲Singleton類並沒有被任何其他的對象所引用,所以這個類在創建後一段時間會被unload,Singleton類的靜態方法就會出現問題,這是由於Java中垃圾收集機製造成的。解決的方法也很容易,只需要爲其創建一個引用就行了。而在JDK1.2以後的版本中,Sun重新定義了Java規範,改正了其垃圾收集機制中的一些問題,這個問題也就不復存在了,這兒指出只是爲了提起大家的主意。








小結:

Command模式用來封裝請求,也描敘了一致性的發送請求的接口,允許你配置客戶端以處理不同的請求,爲程序增添了更大的靈活性。Singleton模式爲提供對象的單一入口提供了幫助。AbstractFactory和FactoryMethod模式在功能上比較類似,都是用來處理對象的創建的,但應用在不同的層面上。在創建型模式中,還有Builder模式和Prototype模式,這兒不打算詳細的討論了,簡單的說,Builder模式用來處理對象創建的細節。在兩個工廠模式中都沒有涉及到對象創建的具體細節,都是通過接口來返回一個給定類型的對象。而Builder模式則需要對創建一個給定類型對象的過程進行建模。這對創建複雜對象時很有用,使得創建對象的算法獨立於對象各個組成部分的創建。而Prototype模式使用原型機制,通過創建簡單原型的拷貝來創建對象。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章