Skip to content
This repository has been archived by the owner on Aug 25, 2019. It is now read-only.

Latest commit

 

History

History
1512 lines (1234 loc) · 79.4 KB

第16章.md

File metadata and controls

1512 lines (1234 loc) · 79.4 KB

第16章 設計模式

本章要向大家介紹重要但卻並不是那麼傳統的“模式”(Pattern)程序設計方法。

在向面向對象程序設計的演化過程中,或許最重要的一步就是“設計模式”(Design Pattern)的問世。它在由Gamma,Helm和Johnson編著的《設計模式》一書中被定義成一個“里程碑”(該書由Addison-Wesley於1995年出版,註釋①)。那本書列出瞭解決這個問題的23種不同的方法。在本章中,我們準備伴隨幾個例子揭示出設計模式的基本概念。這或許能激起您閱讀《設計模式》一書的慾望。事實上,那本書現在已成為幾乎所有OOP程序員都必備的參考書。

①:但警告大家:書中的例子是用C++寫的。

本章的後一部分包含了展示設計進化過程的一個例子,首先是比較原始的方案,經過逐漸發展和改進,慢慢成為更符合邏輯、更為恰當的設計。該程序(仿真垃圾分類)一直都在進化,可將這種進化作為自己設計模式的一個原型——先為特定的問題提出一個適當的方案,再逐步改善,使其成為解決那類問題一種最靈活的方案。

16.1 模式的概念

在最開始,可將模式想象成一種特別聰明、能夠自我適應的手法,它可以解決特定類型的問題。也就是說,它類似一些需要全面認識某個問題的人。在瞭解了問題的方方面面以後,最後提出一套最通用、最靈活的解決方案。具體問題或許是以前見到並解決過的。然而,從前的方案也許並不是最完善的,大家會看到它如何在一個模式裡具體表達出來。

儘管我們稱之為“設計模式”,但它們實際上並不侷限於設計領域。思考“模式”時,應脫離傳統意義上分析、設計以及實現的思考方式。相反,“模式”是在一個程序裡具體表達一套完整的思想,所以它有時可能出現在分析階段或者高級設計階段。這一點是非常有趣的,因為模式具有以代碼形式直接實現的形式,所以可能不希望它在低級設計或者具體實現以前顯露出來(而且事實上,除非真正進入那些階段,否則一般意識不到自己需要一個模式來解決問題)。

模式的基本概念亦可看成是程序設計的基本概念:添加一層新的抽象!只要我們抽象了某些東西,就相當於隔離了特定的細節。而且這後面最引人注目的動機就是“將保持不變的東西身上發生的變化孤立出來”。這樣做的另一個原因是一旦發現程序的某部分由於這樣或那樣的原因可能發生變化,我們一般都想防止那些改變在代碼內部繁衍出其他變化。這樣做不僅可以降低代碼的維護代價,也更便於我們理解(結果同樣是降低開銷)。

為設計出功能強大且易於維護的應用項目,通常最困難的部分就是找出我稱之為“領頭變化”的東西。這意味著需要找出造成系統改變的最重要的東西,或者換一個角度,找出付出代價最高、開銷最大的那一部分。一旦發現了“領頭變化”,就可以為自己定下一個焦點,圍繞它展開自己的設計。

所以設計模式的最終目標就是將代碼中變化的內容隔離開。如果從這個角度觀察,就會發現本書實際已採用了一些設計模式。舉個例子來說,繼承可以想象成一種設計模式(類似一個由編譯器實現的)。在都擁有同樣接口(即保持不變的東西)的對象內部,它允許我們表達行為上的差異(即發生變化的東西)。組合亦可想象成一種模式,因為它允許我們修改——動態或靜態——用於實現類的對象,所以也能修改類的運作方式。

在《設計模式》一書中,大家還能看到另一種模式:“迭代器”(即Iterator,Java 1.0和1.1不負責任地把它叫作Enumeration,即“枚舉”;Java1.2的集合則改回了“迭代器”的稱呼)。當我們在集合裡遍歷,逐個選擇不同的元素時,迭代器可將集合的實現細節有效地隱藏起來。利用迭代器,可以編寫出通用的代碼,以便對一個序列裡的所有元素採取某種操作,同時不必關心這個序列是如何構建的。這樣一來,我們的通用代碼即可伴隨任何能產生迭代器的集合使用。

16.1.1 單例

或許最簡單的設計模式就是“單例”(Singleton),它能提供對象的一個(而且只有一個)實例。單例在Java庫中得到了應用,但下面這個例子顯得更直接一些:

//: SingletonPattern.java
// The Singleton design pattern: you can
// never instantiate more than one.
package c16;

// Since this isn't inherited from a Cloneable
// base class and cloneability isn't added,
// making it final prevents cloneability from
// being added in any derived classes:
final class Singleton {
  private static Singleton s = new Singleton(47);
  private int i;
  private Singleton(int x) { i = x; }
  public static Singleton getHandle() {
    return s;
  }
  public int getValue() { return i; }
  public void setValue(int x) { i = x; }
}

public class SingletonPattern {
  public static void main(String[] args) {
    Singleton s = Singleton.getHandle();
    System.out.println(s.getValue());
    Singleton s2 = Singleton.getHandle();
    s2.setValue(9);
    System.out.println(s.getValue());
    try {
      // Can't do this: compile-time error.
      // Singleton s3 = (Singleton)s2.clone();
    } catch(Exception e) {}
  }
} ///:~

創建單例的關鍵就是防止客戶程序員採用除由我們提供的之外的任何一種方式來創建一個對象。必須將所有構造器都設為private(私有),而且至少要創建一個構造器,以防止編譯器幫我們自動同步一個默認構造器(它會自做聰明地創建成為“友好的”——friendly,而非private)。

此時應決定如何創建自己的對象。在這兒,我們選擇了靜態創建的方式。但亦可選擇等候客戶程序員發出一個創建請求,然後根據他們的要求動態創建。不管在哪種情況下,對象都應該保存為“私有”屬性。我們通過公用方法提供訪問途徑。在這裡,getHandle()會產生指向Singleton的一個引用。剩下的接口(getValue()setValue())屬於普通的類接口。

Java也允許通過克隆(Clone)方式來創建一個對象。在這個例子中,將類設為final可禁止克隆的發生。由於Singleton是從Object直接繼承的,所以clone()方法會保持protected(受保護)屬性,不能夠使用它(強行使用會造成編譯期錯誤)。然而,假如我們是從一個類結構中繼承,那個結構已經重載了clone()方法,使其具有public屬性,並實現了Cloneable,那麼為了禁止克隆,需要重載clone(),並拋出一個CloneNotSupportedException(不支持克隆異常),就象第12章介紹的那樣。亦可重載clone(),並簡單地返回this。那樣做會造成一定的混淆,因為客戶程序員可能錯誤地認為對象尚未克隆,仍然操縱的是原來的那個。

注意我們並不限於只能創建一個對象。亦可利用該技術創建一個有限的對象池。但在那種情況下,可能需要解決池內對象的共享問題。如果不幸真的遇到這個問題,可以自己設計一套方案,實現共享對象的登記與撤消登記。

16.1.2 模式分類

《設計模式》一書討論了23種不同的模式,並依據三個標準分類(所有標準都涉及那些可能發生變化的方面)。這三個標準是:

(1) 創建:對象的創建方式。這通常涉及對象創建細節的隔離,這樣便不必依賴具體類型的對象,所以在新添一種對象類型時也不必改動代碼。

(2) 結構:設計對象,滿足特定的項目限制。這涉及對象與其他對象的連接方式,以保證系統內的改變不會影響到這些連接。

(3) 行為:對程序中特定類型的行動進行操縱的對象。這要求我們將希望採取的操作封裝起來,比如解釋一種語言、實現一個請求、在一個序列中遍歷(就象在迭代器中那樣)或者實現一種算法。本章提供了“觀察器”(Observer)和“訪問器”(Visitor)的模式的例子。

《設計模式》為所有這23種模式都分別使用了一節,隨附的還有大量示例,但大多是用C++編寫的,少數用Smalltalk編寫(如看過這本書,就知道這實際並不是個大問題,因為很容易即可將基本概念從兩種語言翻譯到Java裡)。現在這本書並不打算重複《設計模式》介紹的所有模式,因為那是一本獨立的書,大家應該單獨閱讀。相反,本章只准備給出一些例子,讓大家先對模式有個大致的印象,並理解它們的重要性到底在哪裡。

16.2 觀察器模式

觀察器(Observer)模式解決的是一個相當普通的問題:由於某些對象的狀態發生了改變,所以一組對象都需要更新,那麼該如何解決?在Smalltalk的MVC(模型-視圖-控制器)的“模型-視圖”部分中,或在幾乎等價的“文檔-視圖結構”中,大家可以看到這個問題。現在我們有一些數據(“文檔”)以及多個視圖,假定為一張圖(Plot)和一個文本視圖。若改變了數據,兩個視圖必須知道對自己進行更新,而那正是“觀察器”要負責的工作。這是一種十分常見的問題,它的解決方案已包括進標準的java.util庫中。

在Java中,有兩種類型的對象用來實現觀察器模式。其中,Observable類用於跟蹤那些當發生一個改變時希望收到通知的所有個體——無論“狀態”是否改變。如果有人說“好了,所有人都要檢查自己,並可能要進行更新”,那麼Observable類會執行這個任務——為列表中的每個“人”都調用notifyObservers()方法。notifyObservers()方法屬於基類Observable的一部分。

在觀察器模式中,實際有兩個方面可能發生變化:觀察對象的數量以及更新的方式。也就是說,觀察器模式允許我們同時修改這兩個方面,不會干擾圍繞在它周圍的其他代碼。

下面這個例子類似於第14章的ColorBoxes示例。箱子(Boxes)置於一個屏幕網格中,每個都初始化一種隨機的顏色。此外,每個箱子都“實現”(implement)了“觀察器”(Observer)接口,而且隨一個Observable對象進行了註冊。若點擊一個箱子,其他所有箱子都會收到一個通知,指出一個改變已經發生。這是由於Observable對象會自動調用每個Observer對象的update()方法。在這個方法內,箱子會檢查被點中的那個箱子是否與自己緊鄰。若答案是肯定的,那麼也修改自己的顏色,保持與點中那個箱子的協調。

//: BoxObserver.java
// Demonstration of Observer pattern using
// Java's built-in observer classes.
import java.awt.*;
import java.awt.event.*;
import java.util.*;

// You must inherit a new type of Observable:
class BoxObservable extends Observable {
  public void notifyObservers(Object b) {
    // Otherwise it won't propagate changes:
    setChanged();
    super.notifyObservers(b);
  }
}

public class BoxObserver extends Frame {
  Observable notifier = new BoxObservable();
  public BoxObserver(int grid) {
    setTitle("Demonstrates Observer pattern");
    setLayout(new GridLayout(grid, grid));
    for(int x = 0; x < grid; x++)
      for(int y = 0; y < grid; y++)
        add(new OCBox(x, y, notifier));
  }   
  public static void main(String[] args) {
    int grid = 8;
    if(args.length > 0)
      grid = Integer.parseInt(args[0]);
    Frame f = new BoxObserver(grid);
    f.setSize(500, 400);
    f.setVisible(true);
    f.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
          System.exit(0);
        }
      });
  }
}

class OCBox extends Canvas implements Observer {
  Observable notifier;
  int x, y; // Locations in grid
  Color cColor = newColor();
  static final Color[] colors = {
    Color.black, Color.blue, Color.cyan,
    Color.darkGray, Color.gray, Color.green,
    Color.lightGray, Color.magenta,
    Color.orange, Color.pink, Color.red,
    Color.white, Color.yellow
  };
  static final Color newColor() {
    return colors[
      (int)(Math.random() * colors.length)
    ];
  }
  OCBox(int x, int y, Observable notifier) {
    this.x = x;
    this.y = y;
    notifier.addObserver(this);
    this.notifier = notifier;
    addMouseListener(new ML());
  }
  public void paint(Graphics  g) {
    g.setColor(cColor);
    Dimension s = getSize();
    g.fillRect(0, 0, s.width, s.height);
  }
  class ML extends MouseAdapter {
    public void mousePressed(MouseEvent e) {
      notifier.notifyObservers(OCBox.this);
    }
  }
  public void update(Observable o, Object arg) {
    OCBox clicked = (OCBox)arg;
    if(nextTo(clicked)) {
      cColor = clicked.cColor;
      repaint();
    }
  }
  private final boolean nextTo(OCBox b) {
    return Math.abs(x - b.x) <= 1 &&
           Math.abs(y - b.y) <= 1;
  }
} ///:~

如果是首次查閱Observable的聯機幫助文檔,可能會多少感到有些困惑,因為它似乎表明可以用一個原始的Observable對象來管理更新。但這種說法是不成立的;大家可自己試試——在BoxObserver中,創建一個Observable對象,替換BoxObservable對象,看看會有什麼事情發生。事實上,什麼事情也不會發生。為真正產生效果,必須從Observable繼承,並在派生類代碼的某個地方調用setChanged()。這個方法需要設置changed(已改變)標誌,它意味著當我們調用notifyObservers()的時候,所有觀察器事實上都會收到通知。在上面的例子中,setChanged()只是簡單地在notifyObservers()中調用,大家可依據符合實際情況的任何標準決定何時調用setChanged()

BoxObserver包含了單個Observable對象,名為notifier。每次創建一個OCBox對象時,它都會同notifier聯繫到一起。在OCBox中,只要點擊鼠標,就會發出對notifyObservers()方法的調用,並將被點中的那個對象作為一個參數傳遞進去,使收到消息(用它們的update()方法)的所有箱子都能知道誰被點中了,並據此判斷自己是否也要變動。通過notifyObservers()update()中的代碼的結合,我們可以應付一些非常複雜的局面。

notifyObservers()方法中,表面上似乎觀察器收到通知的方式必須在編譯期間固定下來。然而,只要稍微仔細研究一下上面的代碼,就會發現BoxObserverOCBox中唯一需要留意是否使用BoxObservable的地方就是創建Observable對象的時候——從那時開始,所有東西都會使用基本的Observable接口。這意味著以後若想更改通知方式,可以繼承其他Observable類,並在運行期間交換它們。

16.3 模擬垃圾回收站

這個問題的本質是若將垃圾丟進單個垃圾筒,事實上是未經分類的。但在以後,某些特殊的信息必須恢復,以便對垃圾正確地歸類。在最開始的解決方案中,RTTI扮演了關鍵的角色(詳見第11章)。

這並不是一種普通的設計,因為它增加了一個新的限制。正是這個限制使問題變得非常有趣——它更象我們在工作中碰到的那些非常麻煩的問題。這個額外的限制是:垃圾抵達垃圾回收站時,它們全都是混合在一起的。程序必須為那些垃圾的分類定出一個模型。這正是RTTI發揮作用的地方:我們有大量不知名的垃圾,程序將正確判斷出它們所屬的類型。

//: RecycleA.java
// Recycling with RTTI
package c16.recyclea;
import java.util.*;
import java.io.*;

abstract class Trash {
  private double weight;
  Trash(double wt) { weight = wt; }
  abstract double value();
  double weight() { return weight; }
  // Sums the value of Trash in a bin:
  static void sumValue(Vector bin) {
    Enumeration e = bin.elements();
    double val = 0.0f;
    while(e.hasMoreElements()) {
      // One kind of RTTI:
      // A dynamically-checked cast
      Trash t = (Trash)e.nextElement();
      // Polymorphism in action:
      val += t.weight() * t.value();
      System.out.println(
        "weight of " +
        // Using RTTI to get type
        // information about the class:
        t.getClass().getName() +
        " = " + t.weight());
    }
    System.out.println("Total value = " + val);
  }
}

class Aluminum extends Trash {
  static double val  = 1.67f;
  Aluminum(double wt) { super(wt); }
  double value() { return val; }
  static void value(double newval) {
    val = newval;
  }
}

class Paper extends Trash {
  static double val = 0.10f;
  Paper(double wt) { super(wt); }
  double value() { return val; }
  static void value(double newval) {
    val = newval;
  }
}

class Glass extends Trash {
  static double val = 0.23f;
  Glass(double wt) { super(wt); }
  double value() { return val; }
  static void value(double newval) {
    val = newval;
  }
}

public class RecycleA {
  public static void main(String[] args) {
    Vector bin = new Vector();
    // Fill up the Trash bin:
    for(int i = 0; i < 30; i++)
      switch((int)(Math.random() * 3)) {
        case 0 :
          bin.addElement(new
            Aluminum(Math.random() * 100));
          break;
        case 1 :
          bin.addElement(new
            Paper(Math.random() * 100));
          break;
        case 2 :
          bin.addElement(new
            Glass(Math.random() * 100));
      }
    Vector
      glassBin = new Vector(),
      paperBin = new Vector(),
      alBin = new Vector();
    Enumeration sorter = bin.elements();
    // Sort the Trash:
    while(sorter.hasMoreElements()) {
      Object t = sorter.nextElement();
      // RTTI to show class membership:
      if(t instanceof Aluminum)
        alBin.addElement(t);
      if(t instanceof Paper)
        paperBin.addElement(t);
      if(t instanceof Glass)
        glassBin.addElement(t);
    }
    Trash.sumValue(alBin);
    Trash.sumValue(paperBin);
    Trash.sumValue(glassBin);
    Trash.sumValue(bin);
  }
} ///:~

要注意的第一個地方是package語句:

package c16.recyclea;

這意味著在本書採用的源碼目錄中,這個文件會被置入從c16(代表第16章的程序)分支出來的recyclea子目錄中。第17章的解包工具會負責將其置入正確的子目錄。之所以要這樣做,是因為本章會多次改寫這個特定的例子;它的每個版本都會置入自己的“包”(package)內,避免類名的衝突。

其中創建了幾個Vector對象,用於容納Trash引用。當然,Vector實際容納的是Object(對象),所以它們最終能夠容納任何東西。之所以要它們容納Trash(或者從Trash派生出來的其他東西),唯一的理由是我們需要謹慎地避免放入除Trash以外的其他任何東西。如果真的把某些“錯誤”的東西置入Vector,那麼不會在編譯期得到出錯或警告提示——只能通過運行期的一個異常知道自己已經犯了錯誤。

Trash引用加入後,它們會丟失自己的特定標識信息,只會成為簡單的Object引用(向上轉換)。然而,由於存在多態性的因素,所以在我們通過Enumeration sorter調用動態綁定方法時,一旦結果Object已經轉換回Trash,仍然會發生正確的行為。sumValue()也用一個EnumerationVector中的每個對象進行操作。

表面上持,先把Trash的類型向上轉換到一個集合容納基類型的引用,再回過頭重新向下轉換,這似乎是一種非常愚蠢的做法。為什麼不只是一開始就將垃圾置入適當的容器裡呢?(事實上,這正是撥開“回收”一團迷霧的關鍵)。在這個程序中,我們很容易就可以換成這種做法,但在某些情況下,系統的結構及靈活性都能從向下轉換中得到極大的好處。

該程序已滿足了設計的初衷:它能夠正常工作!只要這是個一次性的方案,就會顯得非常出色。但是,真正有用的程序應該能夠在任何時候解決問題。所以必須問自己這樣一個問題:“如果情況發生了變化,它還能工作嗎?”舉個例子來說,厚紙板現在是一種非常有價值的可回收物品,那麼如何把它集成到系統中呢(特別是程序很大很複雜的時候)?由於前面在switch語句中的類型檢查編碼可能散佈於整個程序,所以每次加入一種新類型時,都必須找到所有那些編碼。若不慎遺漏一個,編譯器除了指出存在一個錯誤之外,不能再提供任何有價值的幫助。

RTTI在這裡使用不當的關鍵是“每種類型都進行了測試”。如果由於類型的子集需要特殊的對待,所以只尋找那個子集,那麼情況就會變得好一些。但假如在一個switch語句中查找每一種類型,那麼很可能錯過一個重點,使最終的代碼很難維護。在下一節中,大家會學習如何逐步對這個程序進行改進,使其顯得越來越靈活。這是在程序設計中一種非常有意義的例子。

16.4 改進設計

《設計模式》書內所有方案的組織都圍繞“程序進化時會發生什麼變化”這個問題展開。對於任何設計來說,這都可能是最重要的一個問題。若根據對這個問題的回答來構造自己的系統,就可以得到兩個方面的結果:系統不僅更易維護(而且更廉價),而且能產生一些能夠重複使用的對象,進而使其他相關係統的構造也變得更廉價。這正是面向對象程序設計的優勢所在,但這一優勢並不是自動體現出來的。它要求對我們對需要解決的問題有全面而且深入的理解。在這一節中,我們準備在系統的逐步改進過程中向大家展示如何做到這一點。

就目前這個回收系統來說,對“什麼會變化”這個問題的回答是非常普通的:更多的類型會加入系統。因此,設計的目標就是儘可能簡化這種類型的添加。在回收程序中,我們準備把涉及特定類型信息的所有地方都封裝起來。這樣一來(如果沒有別的原因),所有變化對那些封裝來說都是在本地進行的。這種處理方式也使代碼剩餘的部分顯得特別清爽。

16.4.1 “製作更多的對象”

這樣便引出了面向對象程序設計時一條常規的準則,我最早是在Grady Booch那裡聽說的:“若設計過於複雜,就製作更多的對象”。儘管聽起來有些曖昧,且簡單得可笑,但這確實是我知道的最有用一條準則(大家以後會注意到“製作更多的對象”經常等同於“添加另一個層次的迂迴”)。一般情況下,如果發現一個地方充斥著大量繁複的代碼,就需要考慮什麼類能使它顯得清爽一些。用這種方式整理系統,往往會得到一個更好的結構,也使程序更加靈活。

首先考慮Trash對象首次創建的地方,這是main()裡的一個switch語句:

    for(int i = 0; i < 30; i++)
      switch((int)(Math.random() * 3)) {
        case 0 :
          bin.addElement(new
            Aluminum(Math.random() * 100));
          break;
        case 1 :
          bin.addElement(new
            Paper(Math.random() * 100));
          break;
        case 2 :
          bin.addElement(new
            Glass(Math.random() * 100));
      }

這些代碼顯然“過於複雜”,也是新類型加入時必須改動代碼的場所之一。如果經常都要加入新類型,那麼更好的方案就是建立一個獨立的方法,用它獲取所有必需的信息,並創建一個引用,指向正確類型的一個對象——已經向上轉換到一個Trash對象。在《設計模式》中,它被粗略地稱呼為“創建模式”。要在這裡應用的特殊模式是Factory方法的一種變體。在這裡,Factory方法屬於Trash的一名static(靜態)成員。但更常見的一種情況是:它屬於派生類中一個被重載的方法。

Factory方法的基本原理是我們將創建對象所需的基本信息傳遞給它,然後返回並等候引用(已經向上轉換至基類型)作為返回值出現。從這時開始,就可以按多態性的方式對待對象了。因此,我們根本沒必要知道所創建對象的準確類型是什麼。事實上,Factory方法會把自己隱藏起來,我們是看不見它的。這樣做可防止不慎的誤用。如果想在沒有多態性的前提下使用對象,必須明確地使用RTTI和指定轉換。

但仍然存在一個小問題,特別是在基類中使用更復雜的方法(不是在這裡展示的那種),且在派生類裡重載(覆蓋)了它的前提下。如果在派生類裡請求的信息要求更多或者不同的參數,那麼該怎麼辦呢?“創建更多的對象”解決了這個問題。為實現Factory方法,Trash類使用了一個新的方法,名為factory。為了將創建數據隱藏起來,我們用一個名為Info的新類包含factory方法創建適當的Trash對象時需要的全部信息。下面是Info一種簡單的實現方式:

class Info {
  int type;
  // Must change this to add another type:
  static final int MAX_NUM = 4;
  double data;
  Info(int typeNum, double dat) {
    type = typeNum % MAX_NUM;
    data = dat;
  }
}

Info對象唯一的任務就是容納用於factory()方法的信息。現在,假如出現了一種特殊情況,factory()需要更多或者不同的信息來新建一種類型的Trash對象,那麼再也不需要改動factory()了。通過添加新的數據和構造器,我們可以修改Info類,或者採用子類處理更典型的面向對象形式。

用於這個簡單示例的factory()方法如下:

  static Trash factory(Info i) {
    switch(i.type) {
      default: // To quiet the compiler
      case 0:
        return new Aluminum(i.data);
      case 1:
        return new Paper(i.data);
      case 2:
        return new Glass(i.data);
      // Two lines here:
      case 3:
        return new Cardboard(i.data);
    }
  }

在這裡,對象的準確類型很容易即可判斷出來。但我們可以設想一些更復雜的情況,factory()將採用一種複雜的算法。無論如何,現在的關鍵是它已隱藏到某個地方,而且我們在添加新類型時知道去那個地方。

新對象在main()中的創建現在變得非常簡單和清爽:

    for(int i = 0; i < 30; i++)
      bin.addElement(
        Trash.factory(
          new Info(
            (int)(Math.random() * Info.MAX_NUM),
            Math.random() * 100)));

我們在這裡創建了一個Info對象,用於將數據傳入factory();後者在內存堆中創建某種Trash對象,並返回添加到Vector bin內的引用。當然,如果改變了參數的數量及類型,仍然需要修改這個語句。但假如Info對象的創建是自動進行的,也可以避免那個麻煩。例如,可將參數的一個Vector傳遞到Info對象的構造器中(或直接傳入一個factory()調用)。這要求在運行期間對參數進行分析與檢查,但確實提供了非常高的靈活程度。

大家從這個代碼可看出Factory要負責解決的“領頭變化”問題:如果向系統添加了新類型(發生了變化),唯一需要修改的代碼在Factory內部,所以Factory將那種變化的影響隔離出來了。

16.4.2 用於原型創建的一個模式

上述設計模式的一個問題是仍然需要一箇中心場所,必須在那裡知道所有類型的對象:在factory()方法內部。如果經常都要向系統添加新類型,factory()方法為每種新類型都要修改一遍。若確實對這個問題感到苦惱,可試試再深入一步,將與類型有關的所有信息——包括它的創建過程——都移入代表那種類型的類內部。這樣一來,每次新添一種類型的時候,需要做的唯一事情就是從一個類繼承。

為將涉及類型創建的信息移入特定類型的Trash裡,必須使用“原型”(prototype)模式(來自《設計模式》那本書)。這裡最基本的想法是我們有一個主控對象序列,為自己感興趣的每種類型都製作一個。這個序列中的對象只能用於新對象的創建,採用的操作類似內建到Java根類Object內部的clone()機制。在這種情況下,我們將克隆方法命名為tClone()。準備創建一個新對象時,要事先收集好某種形式的信息,用它建立我們希望的對象類型。然後在主控序列中遍歷,將手上的信息與主控序列中原型對象內任何適當的信息作對比。若找到一個符合自己需要的,就克隆它。

採用這種方案,我們不必用硬編碼的方式植入任何創建信息。每個對象都知道如何揭示出適當的信息,以及如何對自身進行克隆。所以一種新類型加入系統的時候,factory()方法不需要任何改變。

為解決原型的創建問題,一個方法是添加大量方法,用它們支持新對象的創建。但在Java 1.1中,如果擁有指向Class對象的一個引用,那麼它已經提供了對創建新對象的支持。利用Java 1.1的“反射”(已在第11章介紹)技術,即便我們只有指向Class對象的一個引用,亦可正常地調用一個構造器。這對原型問題的解決無疑是個完美的方案。

原型列表將由指向所有想創建的Class對象的一個引用列表間接地表示。除此之外,假如原型處理失敗,則factory()方法會認為由於一個特定的Class對象不在列表中,所以會嘗試裝載它。通過以這種方式動態裝載原型,Trash類根本不需要知道自己要操縱的是什麼類型。因此,在我們添加新類型時不需要作出任何形式的修改。於是,我們可在本章剩餘的部分方便地重複利用它。

//: Trash.java
// Base class for Trash recycling examples
package c16.trash;
import java.util.*;
import java.lang.reflect.*;

public abstract class Trash {
  private double weight;
  Trash(double wt) { weight = wt; }
  Trash() {}
  public abstract double value();
  public double weight() { return weight; }
  // Sums the value of Trash in a bin:
  public static void sumValue(Vector bin) {
    Enumeration e = bin.elements();
    double val = 0.0f;
    while(e.hasMoreElements()) {
      // One kind of RTTI:
      // A dynamically-checked cast
      Trash t = (Trash)e.nextElement();
      val += t.weight() * t.value();
      System.out.println(
        "weight of " +
        // Using RTTI to get type
        // information about the class:
        t.getClass().getName() +
        " = " + t.weight());
    }
    System.out.println("Total value = " + val);
  }
  // Remainder of class provides support for
  // prototyping:
  public static class PrototypeNotFoundException
      extends Exception {}
  public static class CannotCreateTrashException
      extends Exception {}
  private static Vector trashTypes =
    new Vector();
  public static Trash factory(Info info)
      throws PrototypeNotFoundException,
      CannotCreateTrashException {
    for(int i = 0; i < trashTypes.size(); i++) {
      // Somehow determine the new type
      // to create, and create one:
      Class tc =
        (Class)trashTypes.elementAt(i);
      if (tc.getName().indexOf(info.id) != -1) {
        try {
          // Get the dynamic constructor method
          // that takes a double argument:
          Constructor ctor =
            tc.getConstructor(
              new Class[] {double.class});
          // Call the constructor to create a
          // new object:
          return (Trash)ctor.newInstance(
            new Object[]{new Double(info.data)});
        } catch(Exception ex) {
          ex.printStackTrace();
          throw new CannotCreateTrashException();
        }
      }
    }
    // Class was not in the list. Try to load it,
    // but it must be in your class path!
    try {
      System.out.println("Loading " + info.id);
      trashTypes.addElement(
        Class.forName(info.id));
    } catch(Exception e) {
      e.printStackTrace();
      throw new PrototypeNotFoundException();
    }
    // Loaded successfully. Recursive call
    // should work this time:
    return factory(info);
  }
  public static class Info {
    public String id;
    public double data;
    public Info(String name, double data) {
      id = name;
      this.data = data;
    }
  }
} ///:~

基本Trash類和sumValue()還是象往常一樣。這個類剩下的部分支持原型模式。大家首先會看到兩個內部類(被設為static屬性,使其成為只為代碼組織目的而存在的內部類),它們描述了可能出現的異常。在它後面跟隨的是一個Vector trashTypes,用於容納Class引用。

Trash.factory()中,Info對象idInfo類的另一個版本,與前面討論的不同)內部的String包含了要創建的那種Trash的類型名稱。這個String會與列表中的Class名比較。若存在相符的,那便是要創建的對象。當然,還有很多方法可以決定我們想創建的對象。之所以要採用這種方法,是因為從一個文件讀入的信息可以轉換成對象。

發現自己要創建的Trash(垃圾)種類後,接下來就輪到“反射”方法大顯身手了。getConstructor()方法需要取得自己的參數——由Class引用構成的一個數組。這個數組代表著不同的參數,並按它們正確的順序排列,以便我們查找的構造器使用。在這兒,該數組是用Java 1.1的數組創建語法動態創建的:

new Class[] {double.class}

這個代碼假定所有Trash類型都有一個需要double數值的構造器(注意double.classDouble.class是不同的)。若考慮一種更靈活的方案,亦可調用getConstructors(),令其返回可用構造器的一個數組。

getConstructors()返回的是指向一個Constructor對象的引用(該對象是java.lang.reflect的一部分)。我們用方法newInstance()動態地調用構造器。該方法需要獲取包含了實際參數的一個Object數組。這個數組同樣是按Java 1.1的語法創建的:

new Object[] {new Double(info.data)}

在這種情況下,double必須置入一個封裝(容器)類的內部,使其真正成為這個對象數組的一部分。通過調用newInstance(),會提取出double,但大家可能會覺得稍微有些迷惑——參數既可能是double,也可能是Double,但在調用的時候必須用Double傳遞。幸運的是,這個問題只存在於基本數據類型中間。

理解了具體的過程後,再來創建一個新對象,並且只為它提供一個Class引用,事情就變得非常簡單了。就目前的情況來說,內部循環中的return永遠不會執行,我們在終點就會退出。在這兒,程序動態裝載Class對象,並把它加入trashTypes(垃圾類型)列表,從而試圖糾正這個問題。若仍然找不到真正有問題的地方,同時裝載又是成功的,那麼就重複調用factory方法,重新試一遍。

正如大家會看到的那樣,這種設計模式最大的優點就是不需要改動代碼。無論在什麼情況下,它都能正常地使用(假定所有Trash子類都包含了一個構造器,用以獲取單個double參數)。

(1) Trash子類

為了與原型機制相適應,對Trash每個新子類唯一的要求就是在其中包含了一個構造器,指示它獲取一個double參數。Java 1.1的“反射”機制可負責剩下的所有工作。

下面是不同類型的Trash,每種類型都有它們自己的文件裡,但都屬於Trash包的一部分(同樣地,為了方便在本章內重複使用):

//: Aluminum.java
// The Aluminum class with prototyping
package c16.trash;

public class Aluminum extends Trash {
  private static double val = 1.67f;
  public Aluminum(double wt) { super(wt); }
  public double value() { return val; }
  public static void value(double newVal) {
    val = newVal;
  }
} ///:~

下面是一種新的Trash類型:

//: Cardboard.java
// The Cardboard class with prototyping
package c16.trash;

public class Cardboard extends Trash {
  private static double val = 0.23f;
  public Cardboard(double wt) { super(wt); }
  public double value() { return val; }
  public static void value(double newVal) {
    val = newVal;
  }
} ///:~

可以看出,除構造器以外,這些類根本沒有什麼特別的地方。

(2) 從外部文件中解析出Trash

Trash對象有關的信息將從一個外部文件中讀取。針對Trash的每個方面,文件內列出了所有必要的信息——每行都代表一個方面,採用垃圾(廢品)名稱:值的固定格式。例如:

c16.Trash.Glass:54
c16.Trash.Paper:22
c16.Trash.Paper:11
c16.Trash.Glass:17
c16.Trash.Aluminum:89
c16.Trash.Paper:88
c16.Trash.Aluminum:76
c16.Trash.Cardboard:96
c16.Trash.Aluminum:25
c16.Trash.Aluminum:34
c16.Trash.Glass:11
c16.Trash.Glass:68
c16.Trash.Glass:43
c16.Trash.Aluminum:27
c16.Trash.Cardboard:44
c16.Trash.Aluminum:18
c16.Trash.Paper:91
c16.Trash.Glass:63
c16.Trash.Glass:50
c16.Trash.Glass:80
c16.Trash.Aluminum:81
c16.Trash.Cardboard:12
c16.Trash.Glass:12
c16.Trash.Glass:54
c16.Trash.Aluminum:36
c16.Trash.Aluminum:93
c16.Trash.Glass:93
c16.Trash.Paper:80
c16.Trash.Glass:36
c16.Trash.Glass:12
c16.Trash.Glass:60
c16.Trash.Paper:66
c16.Trash.Aluminum:36
c16.Trash.Cardboard:22

注意在給定類名的時候,類路徑必須包含在內,否則就找不到類。

為解析它,每一行內容都會讀入,並用字符串方法indexOf()來建立:的一個索引。首先用字符串方法substring()取出垃圾的類型名稱,接著用一個靜態方法Double.valueOf()取得相應的值,並轉換成一個double值。trim()方法則用於刪除字符串兩頭的多餘空格。

Trash解析器置入單獨的文件中,因為本章將不斷地用到它。如下所示:

//: ParseTrash.java
// Open a file and parse its contents into
// Trash objects, placing each into a Vector
package c16.trash;
import java.util.*;
import java.io.*;

public class ParseTrash {
  public static void
  fillBin(String filename, Fillable bin) {
    try {
      BufferedReader data =
        new BufferedReader(
          new FileReader(filename));
      String buf;
      while((buf = data.readLine())!= null) {
        String type = buf.substring(0,
          buf.indexOf(':')).trim();
        double weight = Double.valueOf(
          buf.substring(buf.indexOf(':') + 1)
          .trim()).doubleValue();
        bin.addTrash(
          Trash.factory(
            new Trash.Info(type, weight)));
      }
      data.close();
    } catch(IOException e) {
      e.printStackTrace();
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
  // Special case to handle Vector:
  public static void
  fillBin(String filename, Vector bin) {
    fillBin(filename, new FillableVector(bin));
  }
} ///:~

RecycleA.java中,我們用一個Vector容納Trash對象。然而,亦可考慮採用其他集合類型。為做到這一點,fillBin()的第一個版本將獲取指向一個Fillable的引用。後者是一個接口,用於支持一個名為addTrash()的方法:

//: Fillable.java
// Any object that can be filled with Trash
package c16.trash;

public interface Fillable {
  void addTrash(Trash t);
} ///:~

支持該接口的所有東西都能伴隨fillBin使用。當然,Vector並未實現Fillable,所以它不能工作。由於Vector將在大多數例子中應用,所以最好的做法是添加另一個重載的fillBin()方法,令其以一個Vector作為參數。利用一個適配器(Adapter)類,這個Vector可作為一個Fillable對象使用:

//: FillableVector.java
// Adapter that makes a Vector Fillable
package c16.trash;
import java.util.*;

public class FillableVector implements Fillable {
  private Vector v;
  public FillableVector(Vector vv) { v = vv; }
  public void addTrash(Trash t) {
    v.addElement(t);
  }
} ///:~

可以看到,這個類唯一的任務就是負責將FillableaddTrash()VectoraddElement()方法連接起來。利用這個類,已重載的fillBin()方法可在ParseTrash.java中伴隨一個Vector使用:

  public static void
  fillBin(String filename, Vector bin) {
    fillBin(filename, new FillableVector(bin));
  }

這種方案適用於任何頻繁用到的集合類。除此以外,集合類還可提供它自己的適配器類,並實現Fillable(稍後即可看到,在DynaTrash.java中)。

(3) 原型機制的重複應用

現在,大家可以看到採用原型技術的、修訂過的RecycleA.java版本了:

//: RecycleAP.java
// Recycling with RTTI and Prototypes
package c16.recycleap;
import c16.trash.*;
import java.util.*;

public class RecycleAP {
  public static void main(String[] args) {
    Vector bin = new Vector();
    // Fill up the Trash bin:
    ParseTrash.fillBin("Trash.dat", bin);
    Vector
      glassBin = new Vector(),
      paperBin = new Vector(),
      alBin = new Vector();
    Enumeration sorter = bin.elements();
    // Sort the Trash:
    while(sorter.hasMoreElements()) {
      Object t = sorter.nextElement();
      // RTTI to show class membership:
      if(t instanceof Aluminum)
        alBin.addElement(t);
      if(t instanceof Paper)
        paperBin.addElement(t);
      if(t instanceof Glass)
        glassBin.addElement(t);
    }
    Trash.sumValue(alBin);
    Trash.sumValue(paperBin);
    Trash.sumValue(glassBin);
    Trash.sumValue(bin);
  }
} ///:~

所有Trash對象——以及ParseTrash及支撐類——現在都成為名為c16.trash的一個包的一部分,所以它們可以簡單地導入。

無論打開包含了Trash描述信息的數據文件,還是對那個文件進行解析,所有涉及到的操作均已封裝到static(靜態)方法ParseTrash.fillBin()裡。所以它現在已經不是我們設計過程中要注意的一個重點。在本章剩餘的部分,大家經常都會看到無論添加的是什麼類型的新類,ParseTrash.fillBin()都會持續工作,不會發生改變,這無疑是一種優良的設計模式。

提到對象的創建,這一方案確實已將新類型加入系統所需的變動嚴格地“本地化”了。但在使用RTTI的過程中,卻存在著一個嚴重的問題,這裡已明確地顯露出來。程序表面上工作得很好,但卻永遠偵測到不能“硬紙板”(Cardboard)這種新的廢品類型——即使列表裡確實有一個硬紙板類型!之所以會出現這種情況,完全是由於使用了RTTI的緣故。RTTI只會查找那些我們告訴它查找的東西。RTTI在這裡錯誤的用法是“系統中的每種類型”都進行了測試,而不是僅測試一種類型或者一個類型子集。正如大家以後會看到的那樣,在測試每一種類型時可換用其他方式來運用多態性特徵。但假如以這種形式過多地使用RTTI,而且又在自己的系統裡添加了一種新類型,很容易就會忘記在程序裡作出適當的改動,從而埋下以後難以發現的Bug。因此,在這種情況下避免使用RTTI是很有必要的,這並不僅僅是為了表面好看——也是為了產生更易維護的代碼。

16.5 抽象的應用

走到這一步,接下來該考慮一下設計模式剩下的部分了——在哪裡使用類?既然歸類到垃圾箱的辦法非常不雅且過於暴露,為什麼不隔離那個過程,把它隱藏到一個類裡呢?這就是著名的“如果必須做不雅的事情,至少應將其本地化到一個類裡”規則。看起來就象下面這樣:

現在,只要一種新類型的Trash加入方法,對TrashSorter對象的初始化就必須變動。可以想象,TrashSorter類看起來應該象下面這個樣子:

class TrashSorter extends Vector {
void sort(Trash t) { /* ... */ }
}

也就是說,TrashSorter是由一系列引用構成的Vector(系列),而那些引用指向的又是由Trash引用構成的Vector;利用addElement(),可以安裝新的TrashSorter,如下所示:

TrashSorter ts = new TrashSorter();
ts.addElement(new Vector());

但是現在,sort()卻成為一個問題。用靜態方式編碼的方法如何應付一種新類型加入的事實呢?為解決這個問題,必須從sort()裡將類型信息刪除,使其需要做的所有事情就是調用一個通用方法,用它照料涉及類型處理的所有細節。這當然是對一個動態綁定方法進行描述的另一種方式。所以sort()會在序列中簡單地遍歷,併為每個Vector都調用一個動態綁定方法。由於這個方法的任務是收集它感興趣的垃圾片,所以稱之為grab(Trash)。結構現在變成了下面這樣:

其中,TrashSorter需要調用每個grab()方法;然後根據當前Vector容納的是什麼類型,會獲得一個不同的結果。也就是說,Vector必須留意自己容納的類型。解決這個問題的傳統方法是創建一個基礎“Trash bin”(垃圾筒)類,併為希望容納的每個不同的類型都繼承一個新的派生類。若Java有一個參數化的類型機制,那就也許是最直接的方法。但對於這種機制應該為我們構建的各個類,我們不應該進行麻煩的手工編碼,以後的“觀察”方式提供了一種更好的編碼方式。

OOP設計一條基本的準則是“為狀態的變化使用數據成員,為行為的變化使用多性形”。對於容納Paper(紙張)的Vector,以及容納Glass(玻璃)的Vector,大家最開始或許會認為分別用於它們的grab()方法肯定會產生不同的行為。但具體如何卻完全取決於類型,而不是其他什麼東西。可將其解釋成一種不同的狀態,而且由於Java有一個類可表示類型(Class),所以可用它判斷特定的Tbin要容納什麼類型的Trash

用於Tbin的構造器要求我們為其傳遞自己選擇的一個Class。這樣做可告訴Vector它希望容納的是什麼類型。隨後,grab()方法用Class BinType和RTTI來檢查我們傳遞給它的Trash對象是否與它希望收集的類型相符。 下面列出完整的解決方案。設定為註釋的編號(如1)便於大家對照程序後面列出的說明。

//: RecycleB.java
// Adding more objects to the recycling problem
package c16.recycleb;
import c16.trash.*;
import java.util.*;

// A vector that admits only the right type:
class Tbin extends Vector {
  Class binType;
  Tbin(Class binType) {
    this.binType = binType;
  }
  boolean grab(Trash t) {
    // Comparing class types:
    if(t.getClass().equals(binType)) {
      addElement(t);
      return true; // Object grabbed
    }
    return false; // Object not grabbed
  }
}

class TbinList extends Vector { //(*1*)
  boolean sort(Trash t) {
    Enumeration e = elements();
    while(e.hasMoreElements()) {
      Tbin bin = (Tbin)e.nextElement();
      if(bin.grab(t)) return true;
    }
    return false; // bin not found for t
  }
  void sortBin(Tbin bin) { // (*2*)
    Enumeration e = bin.elements();
    while(e.hasMoreElements())
      if(!sort((Trash)e.nextElement()))
        System.out.println("Bin not found");
  }
}

public class RecycleB {
  static Tbin bin = new Tbin(Trash.class);
  public static void main(String[] args) {
    // Fill up the Trash bin:
    ParseTrash.fillBin("Trash.dat", bin);

    TbinList trashBins = new TbinList();
    trashBins.addElement(
      new Tbin(Aluminum.class));
    trashBins.addElement(
      new Tbin(Paper.class));
    trashBins.addElement(
      new Tbin(Glass.class));
    // add one line here: (*3*)
    trashBins.addElement(
      new Tbin(Cardboard.class));

    trashBins.sortBin(bin); // (*4*)

    Enumeration e = trashBins.elements();
    while(e.hasMoreElements()) {
      Tbin b = (Tbin)e.nextElement();
      Trash.sumValue(b);
    }
    Trash.sumValue(bin);
  }
} ///:~

(1) TbinList容納一系列Tbin引用,所以在查找與我們傳遞給它的Trash對象相符的情況時,sort()能通過Tbin繼承。

(2) sortBin()允許我們將一個完整的Tbin傳遞進去,而且它會在Tbin裡遍歷,挑選出每種Trash,並將其歸類到特定的Tbin中。請注意這些代碼的通用性:新類型加入時,它本身不需要任何改動。只要新類型加入(或發生其他事件)時大量代碼都不需要變化,就表明我們設計的是一個容易擴展的系統。

(3) 現在可以體會添加新類型有多麼容易了。為支持添加,只需要改動幾行代碼。如確實有必要,甚至可以進一步地改進設計,使更多的代碼都保持“固定”。

(4) 一個方法調用使bin的內容歸類到對應的、特定類型的垃圾筒裡。

16.6 多重分發

上述設計模式肯定是令人滿意的。系統內新類型的加入涉及添加或修改不同的類,但沒有必要在系統內對代碼作大範圍的改動。除此以外,RTTI並不象它在RecycleA.java裡那樣被不當地使用。然而,我們仍然有可能更深入一步,以最“純”的角度來看待RTTI, 考慮如何在垃圾分類系統中將它完全消滅。

為達到這個目標,首先必須認識到:對所有與不同類型有特殊關聯的活動來說——比如偵測一種垃圾的具體類型,並把它置入適當的垃圾筒裡——這些活動都應當通過多態性以及動態綁定加以控制。

以前的例子都是先按類型排序,再對屬於某種特殊類型的一系列元素進行操作。現在一旦需要操作特定的類型,就請先停下來想一想。事實上,多態性(動態綁定的方法調用)整個的宗旨就是幫我們管理與不同類型有特殊關聯的信息。既然如此,為什麼還要自己去檢查類型呢?

答案在於大家或許不以為然的一個道理:Java只執行單一分發。也就是說,假如對多個類型未知的對象執行某項操作,Java只會為那些類型中的一種調用動態綁定機制。這當然不能解決問題,所以最後不得不人工判斷某些類型,才能有效地產生自己的動態綁定行為。

為解決這個缺陷,我們需要用到“多重分發”機制,這意味著需要建立一個配置,使單一方法調用能產生多個動態方法調用,從而在一次處理過程中正確判斷出多種類型。為達到這個要求,需要對多個類型結構進行操作:每一次分發都需要一個類型結構。下面的例子將對兩個結構進行操作:現有的Trash系列以及由垃圾筒(Trash Bin)的類型構成的一個系列——不同的垃圾或廢品將置入這些筒內。第二個分級結構並非絕對顯然的。在這種情況下,我們需要人為地創建它,以執行多重分發(由於本例只涉及兩次分發,所以稱為“雙重分發”)。

16.6.1 實現雙重分發

記住多態性只能通過方法調用才能表現出來,所以假如想使雙重分發正確進行,必須執行兩個方法調用:在每種結構中都用一個來判斷其中的類型。在Trash結構中,將使用一個新的方法調用addToBin(),它採用的參數是由TypeBin構成的一個數組。那個方法將在數組中遍歷,嘗試將自己加入適當的垃圾筒,這裡正是雙重分發發生的地方。

新建立的分級結構是TypeBin,其中包含了它自己的一個方法,名為add(),而且也應用了多態性。但要注意一個新特點:add()已進行了“重載”處理,可接受不同的垃圾類型作為參數。因此,雙重滿足機制的一個關鍵點是它也要涉及到重載。

程序的重新設計也帶來了一個問題:現在的基類Trash必須包含一個addToBin()方法。為解決這個問題,一個最直接的辦法是複製所有代碼,並修改基類。然而,假如沒有對源碼的控制權,那麼還有另一個辦法可以考慮:將addToBin()方法置入一個接口內部,保持Trash不變,並繼承新的、特殊的類型AluminumPaperGlass以及Cardboard。我們在這裡準備採取後一個辦法。

這個設計模式中用到的大多數類都必須設為public(公用)屬性,所以它們放置於自己的類內。下面列出接口代碼:

//: TypedBinMember.java
// An interface for adding the double dispatching
// method to the trash hierarchy without
// modifying the original hierarchy.
package c16.doubledispatch;

interface TypedBinMember {
  // The new method:
  boolean addToBin(TypedBin[] tb);
} ///:~

AluminumPaperGlass以及Cardboard每個特定的子類型內,都會實現接口TypeBinMemberaddToBin()方法,但每種情況下使用的代碼“似乎”都是完全一樣的:

//: DDAluminum.java
// Aluminum for double dispatching
package c16.doubledispatch;
import c16.trash.*;

public class DDAluminum extends Aluminum
    implements TypedBinMember {
  public DDAluminum(double wt) { super(wt); }
  public boolean addToBin(TypedBin[] tb) {
    for(int i = 0; i < tb.length; i++)
      if(tb[i].add(this))
        return true;
    return false;
  }
} ///:~
//: DDPaper.java
// Paper for double dispatching
package c16.doubledispatch;
import c16.trash.*;

public class DDPaper extends Paper
    implements TypedBinMember {
  public DDPaper(double wt) { super(wt); }
  public boolean addToBin(TypedBin[] tb) {
    for(int i = 0; i < tb.length; i++)
      if(tb[i].add(this))
        return true;
    return false;
  }
} ///:~
//: DDGlass.java
// Glass for double dispatching
package c16.doubledispatch;
import c16.trash.*;

public class DDGlass extends Glass
    implements TypedBinMember {
  public DDGlass(double wt) { super(wt); }
  public boolean addToBin(TypedBin[] tb) {
    for(int i = 0; i < tb.length; i++)
      if(tb[i].add(this))
        return true;
    return false;
  }
} ///:~
//: DDCardboard.java
// Cardboard for double dispatching
package c16.doubledispatch;
import c16.trash.*;

public class DDCardboard extends Cardboard
    implements TypedBinMember {
  public DDCardboard(double wt) { super(wt); }
  public boolean addToBin(TypedBin[] tb) {
    for(int i = 0; i < tb.length; i++)
      if(tb[i].add(this))
        return true;
    return false;
  }
} ///:~

每個addToBin()內的代碼會為數組中的每個TypeBin對象調用add()。但請注意參數:this。對Trash的每個子類來說,this的類型都是不同的,所以不能認為代碼“完全”一樣——儘管以後在Java里加入參數化類型機制後便可認為一樣。這是雙重分發的第一個部分,因為一旦進入這個方法內部,便可知道到底是AluminumPaper,還是其他什麼垃圾類型。在對add()的調用過程中,這種信息是通過this的類型傳遞的。編譯器會分析出對add()正確的重載版本的調用。但由於tb[i]會產生指向基類型TypeBin的一個引用,所以最終會調用一個不同的方法——具體什麼方法取決於當前選擇的TypeBin的類型。那就是第二次分發。

下面是TypeBin的基類:

//: TypedBin.java
// Vector that knows how to grab the right type
package c16.doubledispatch;
import c16.trash.*;
import java.util.*;

public abstract class TypedBin {
  Vector v = new Vector();
  protected boolean addIt(Trash t) {
    v.addElement(t);
    return true;
  }
  public Enumeration elements() {
    return v.elements();
  }
  public boolean add(DDAluminum a) {
    return false;
  }
  public boolean add(DDPaper a) {
    return false;
  }
  public boolean add(DDGlass a) {
    return false;
  }
  public boolean add(DDCardboard a) {
    return false;
  }
} ///:~

可以看到,重載的add()方法全都會返回false。如果未在派生類裡對方法進行重載,它就會一直返回false,而且調用者(目前是addToBin())會認為當前Trash對象尚未成功加入一個集合,所以會繼續查找正確的集合。

TypeBin的每一個子類中,都只有一個重載的方法會被重載——具體取決於準備創建的是什麼垃圾筒類型。舉個例子來說,CardboardBin會重載add(DDCardboard)。重載的方法會將垃圾對象加入它的集合,並返回true。而CardboardBin中剩餘的所有add()方法都會繼續返回false,因為它們尚未重載。事實上,假如在這裡採用了參數化類型機制,Java代碼的自動創建就要方便得多(使用C++的“模板”,我們不必費事地為子類編碼,或者將addToBin()方法置入Trash裡;Java在這方面尚有待改進)。

由於對這個例子來說,垃圾的類型已經定製並置入一個不同的目錄,所以需要用一個不同的垃圾數據文件令其運轉起來。下面是一個示範性的DDTrash.dat

c16.DoubleDispatch.DDGlass:54
c16.DoubleDispatch.DDPaper:22
c16.DoubleDispatch.DDPaper:11
c16.DoubleDispatch.DDGlass:17
c16.DoubleDispatch.DDAluminum:89
c16.DoubleDispatch.DDPaper:88
c16.DoubleDispatch.DDAluminum:76
c16.DoubleDispatch.DDCardboard:96
c16.DoubleDispatch.DDAluminum:25
c16.DoubleDispatch.DDAluminum:34
c16.DoubleDispatch.DDGlass:11
c16.DoubleDispatch.DDGlass:68
c16.DoubleDispatch.DDGlass:43
c16.DoubleDispatch.DDAluminum:27
c16.DoubleDispatch.DDCardboard:44
c16.DoubleDispatch.DDAluminum:18
c16.DoubleDispatch.DDPaper:91
c16.DoubleDispatch.DDGlass:63
c16.DoubleDispatch.DDGlass:50
c16.DoubleDispatch.DDGlass:80
c16.DoubleDispatch.DDAluminum:81
c16.DoubleDispatch.DDCardboard:12
c16.DoubleDispatch.DDGlass:12
c16.DoubleDispatch.DDGlass:54
c16.DoubleDispatch.DDAluminum:36
c16.DoubleDispatch.DDAluminum:93
c16.DoubleDispatch.DDGlass:93
c16.DoubleDispatch.DDPaper:80
c16.DoubleDispatch.DDGlass:36
c16.DoubleDispatch.DDGlass:12
c16.DoubleDispatch.DDGlass:60
c16.DoubleDispatch.DDPaper:66
c16.DoubleDispatch.DDAluminum:36
c16.DoubleDispatch.DDCardboard:22

下面列出程序剩餘的部分:

//: DoubleDispatch.java
// Using multiple dispatching to handle more
// than one unknown type during a method call.
package c16.doubledispatch;
import c16.trash.*;
import java.util.*;

class AluminumBin extends TypedBin {
  public boolean add(DDAluminum a) {
    return addIt(a);
  }
}

class PaperBin extends TypedBin {
  public boolean add(DDPaper a) {
    return addIt(a);
  }
}

class GlassBin extends TypedBin {
  public boolean add(DDGlass a) {
    return addIt(a);
  }
}

class CardboardBin extends TypedBin {
  public boolean add(DDCardboard a) {
    return addIt(a);
  }
}

class TrashBinSet {
  private TypedBin[] binSet = {
    new AluminumBin(),
    new PaperBin(),
    new GlassBin(),
    new CardboardBin()
  };
  public void sortIntoBins(Vector bin) {
    Enumeration e = bin.elements();
    while(e.hasMoreElements()) {
      TypedBinMember t =
        (TypedBinMember)e.nextElement();
      if(!t.addToBin(binSet))
        System.err.println("Couldn't add " + t);
    }
  }
  public TypedBin[] binSet() { return binSet; }
}

public class DoubleDispatch {
  public static void main(String[] args) {
    Vector bin = new Vector();
    TrashBinSet bins = new TrashBinSet();
    // ParseTrash still works, without changes:
    ParseTrash.fillBin("DDTrash.dat", bin);
    // Sort from the master bin into the
    // individually-typed bins:
    bins.sortIntoBins(bin);
    TypedBin[] tb = bins.binSet();
    // Perform sumValue for each bin...
    for(int i = 0; i < tb.length; i++)
      Trash.sumValue(tb[i].v);
    // ... and for the master bin
    Trash.sumValue(bin);
  }
} ///:~

其中,TrashBinSet封裝了各種不同類型的TypeBin,同時還有sortIntoBins()方法。所有雙重分發事件都會在那個方法裡發生。可以看到,一旦設置好結構,再歸類成各種TypeBin的工作就變得十分簡單了。除此以外,兩個動態方法調用的效率可能也比其他排序方法高一些。

注意這個系統的方便性主要體現在main()中,同時還要注意到任何特定的類型信息在main()中都是完全獨立的。只與Trash基類接口通信的其他所有方法都不會受到Trash類中發生的改變的幹擾。

添加新類型需要作出的改動是完全孤立的:我們隨同addToBin()方法繼承Trash的新類型,然後繼承一個新的TypeBin(這實際只是一個副本,可以簡單地編輯),最後將一種新類型加入TrashBinSet的集合初化化過程。

16.7 訪問器模式

接下來,讓我們思考如何將具有完全不同目標的一個設計模式應用到垃圾歸類系統。

對這個模式,我們不再關心在系統中加入新型Trash時的優化。事實上,這個模式使新型Trash的添加顯得更加複雜。假定我們有一個基本類結構,它是固定不變的;它或許來自另一個開發者或公司,我們無權對那個結構進行任何修改。然而,我們又希望在那個結構里加入新的多態性方法。這意味著我們一般必須在基類的接口裡添加某些東西。因此,我們目前面臨的困境是一方面需要向基類添加方法,另一方面又不能改動基類。怎樣解決這個問題呢?

“訪問器”(Visitor)模式使我們能擴展基本類型的接口,方法是創建類型為Visitor的一個獨立的類結構,對以後需對基本類型採取的操作進行虛擬。基本類型的任務就是簡單地“接收”訪問器,然後調用訪問器的動態綁定方法。看起來就象下面這樣:

現在,假如v是一個指向Aluminum(鋁製品)的Visitable引用,那麼下述代碼:

PriceVisitor pv = new PriceVisitor();
v.accept(pv);

會造成兩個多態性方法調用:第一個會選擇accept()Aluminum版本;第二個則在accept()裡——用基類Visitor引用v動態調用visit()的特定版本時。

這種配置意味著可採取Visitor的新子類的形式將新的功能添加到系統裡,沒必要接觸Trash結構。這就是“訪問器”模式最主要的優點:可為一個類結構添加新的多態性功能,同時不必改動結構——只要安裝好了accept()方法。注意這個優點在這兒是有用的,但並不一定是我們在任何情況下的首選方案。所以在最開始的時候,就要判斷這到底是不是自己需要的方案。

現在注意一件沒有做成的事情:訪問器方案防止了從主控Trash序列向單獨類型序列的歸類。所以我們可將所有東西都留在單主控序列中,只需用適當的訪問器通過那個序列傳遞,即可達到希望的目標。儘管這似乎並非訪問器模式的本意,但確實讓我們達到了很希望達到的一個目標(避免使用RTTI)。

訪問器模式中的雙生分發負責同時判斷Trash以及Visitor的類型。在下面的例子中,大家可看到Visitor的兩種實現方式:PriceVisitor用於判斷總計及價格,而WeightVisitor用於跟蹤重量。

可以看到,所有這些都是用回收程序一個新的、改進過的版本實現的。而且和DoubleDispatch.java一樣,Trash類被保持孤立,並創建一個新接口來添加accept()方法:

//: Visitable.java
// An interface to add visitor functionality to
// the Trash hierarchy without modifying the
// base class.
package c16.trashvisitor;
import c16.trash.*;

interface Visitable {
  // The new method:
  void accept(Visitor v);
} ///:~

AluminumPaperGlass以及Cardboard的子類型實現了accept()方法:

//: VAluminum.java
// Aluminum for the visitor pattern
package c16.trashvisitor;
import c16.trash.*;

public class VAluminum extends Aluminum
    implements Visitable {
  public VAluminum(double wt) { super(wt); }
  public void accept(Visitor v) {
    v.visit(this);
  }
} ///:~
//: VPaper.java
// Paper for the visitor pattern
package c16.trashvisitor;
import c16.trash.*;

public class VPaper extends Paper
    implements Visitable {
  public VPaper(double wt) { super(wt); }
  public void accept(Visitor v) {
    v.visit(this);
  }
} ///:~
//: VGlass.java
// Glass for the visitor pattern
package c16.trashvisitor;
import c16.trash.*;

public class VGlass extends Glass
    implements Visitable {
  public VGlass(double wt) { super(wt); }
  public void accept(Visitor v) {
    v.visit(this);
  }
} ///:~
//: VCardboard.java
// Cardboard for the visitor pattern
package c16.trashvisitor;
import c16.trash.*;

public class VCardboard extends Cardboard
    implements Visitable {
  public VCardboard(double wt) { super(wt); }
  public void accept(Visitor v) {
    v.visit(this);
  }
} ///:~

由於Visitor基類沒有什麼需要實在的東西,可將其創建成一個接口:

//: Visitor.java
// The base interface for visitors
package c16.trashvisitor;
import c16.trash.*;

interface Visitor {
  void visit(VAluminum a);
  void visit(VPaper p);
  void visit(VGlass g);
  void visit(VCardboard c);
} ///:~

c16.TrashVisitor.VGlass:54
c16.TrashVisitor.VPaper:22
c16.TrashVisitor.VPaper:11
c16.TrashVisitor.VGlass:17
c16.TrashVisitor.VAluminum:89
c16.TrashVisitor.VPaper:88
c16.TrashVisitor.VAluminum:76
c16.TrashVisitor.VCardboard:96
c16.TrashVisitor.VAluminum:25
c16.TrashVisitor.VAluminum:34
c16.TrashVisitor.VGlass:11
c16.TrashVisitor.VGlass:68
c16.TrashVisitor.VGlass:43
c16.TrashVisitor.VAluminum:27
c16.TrashVisitor.VCardboard:44
c16.TrashVisitor.VAluminum:18
c16.TrashVisitor.VPaper:91
c16.TrashVisitor.VGlass:63
c16.TrashVisitor.VGlass:50
c16.TrashVisitor.VGlass:80
c16.TrashVisitor.VAluminum:81
c16.TrashVisitor.VCardboard:12
c16.TrashVisitor.VGlass:12
c16.TrashVisitor.VGlass:54
c16.TrashVisitor.VAluminum:36
c16.TrashVisitor.VAluminum:93
c16.TrashVisitor.VGlass:93
c16.TrashVisitor.VPaper:80
c16.TrashVisitor.VGlass:36
c16.TrashVisitor.VGlass:12
c16.TrashVisitor.VGlass:60
c16.TrashVisitor.VPaper:66
c16.TrashVisitor.VAluminum:36
c16.TrashVisitor.VCardboard:22

程序剩餘的部分將創建特定的Visitor類型,並通過一個Trash對象列表發送它們:

//: TrashVisitor.java
// The "visitor" pattern
package c16.trashvisitor;
import c16.trash.*;
import java.util.*;

// Specific group of algorithms packaged
// in each implementation of Visitor:
class PriceVisitor implements Visitor {
  private double alSum; // Aluminum
  private double pSum; // Paper
  private double gSum; // Glass
  private double cSum; // Cardboard
  public void visit(VAluminum al) {
    double v = al.weight() * al.value();
    System.out.println(
      "value of Aluminum= " + v);
    alSum += v;
  }
  public void visit(VPaper p) {
    double v = p.weight() * p.value();
    System.out.println(
      "value of Paper= " + v);
    pSum += v;
  }
  public void visit(VGlass g) {
    double v = g.weight() * g.value();
    System.out.println(
      "value of Glass= " + v);
    gSum += v;
  }
  public void visit(VCardboard c) {
    double v = c.weight() * c.value();
    System.out.println(
      "value of Cardboard = " + v);
    cSum += v;
  }
  void total() {
    System.out.println(
      "Total Aluminum: $" + alSum + "\n" +
      "Total Paper: $" + pSum + "\n" +
      "Total Glass: $" + gSum + "\n" +
      "Total Cardboard: $" + cSum);
  }
}

class WeightVisitor implements Visitor {
  private double alSum; // Aluminum
  private double pSum; // Paper
  private double gSum; // Glass
  private double cSum; // Cardboard
  public void visit(VAluminum al) {
    alSum += al.weight();
    System.out.println("weight of Aluminum = "
        + al.weight());
  }
  public void visit(VPaper p) {
    pSum += p.weight();
    System.out.println("weight of Paper = "
        + p.weight());
  }
  public void visit(VGlass g) {
    gSum += g.weight();
    System.out.println("weight of Glass = "
        + g.weight());
  }
  public void visit(VCardboard c) {
    cSum += c.weight();
    System.out.println("weight of Cardboard = "
        + c.weight());
  }
  void total() {
    System.out.println("Total weight Aluminum:"
        + alSum);
    System.out.println("Total weight Paper:"
        + pSum);
    System.out.println("Total weight Glass:"
        + gSum);
    System.out.println("Total weight Cardboard:"
        + cSum);
  }
}

public class TrashVisitor {
  public static void main(String[] args) {
    Vector bin = new Vector();
    // ParseTrash still works, without changes:
    ParseTrash.fillBin("VTrash.dat", bin);
    // You could even iterate through
    // a list of visitors!
    PriceVisitor pv = new PriceVisitor();
    WeightVisitor wv = new WeightVisitor();
    Enumeration it = bin.elements();
    while(it.hasMoreElements()) {
      Visitable v = (Visitable)it.nextElement();
      v.accept(pv);
      v.accept(wv);
    }
    pv.total();
    wv.total();
  }
} ///:~

注意main()的形狀已再次發生了變化。現在只有一個垃圾(Trash)筒。兩個Visitor對象被接收到序列中的每個元素內,它們會完成自己份內的工作。Visitor跟蹤它們自己的內部數據,計算出總重和價格。

最好,將東西從序列中取出的時候,除了不可避免地向Trash轉換以外,再沒有運行期的類型驗證。若在Java裡實現了參數化類型,甚至那個轉換操作也可以避免。

對比之前介紹過的雙重分發方案,區分這兩種方案的一個辦法是:在雙重分發方案中,每個子類創建時只會重載其中的一個重載方法,即add()。而在這裡,每個重載的visit()方法都必須在Visitor的每個子類中進行重載。

(1) 更多的結合?

這裡還有其他許多代碼,Trash結構和Visitor結構之間存在著明顯的“結合”(Coupling)關係。然而,在它們所代表的類集內部,也存在著高度的凝聚力:都只做一件事情(Trash描述垃圾或廢品,而Visitor描述對垃圾採取什麼行動)。作為一套優秀的設計模式,這無疑是個良好的開端。當然就目前的情況來說,只有在我們添加新的Visitor類型時才能體會到它的好處。但在添加新類型的Trash時,它卻顯得有些礙手礙腳。

類與類之間低度的結合與類內高度的凝聚無疑是一個重要的設計目標。但只要稍不留神,就可能妨礙我們得到一個本該更出色的設計。從表面看,有些類不可避免地相互間存在著一些“親密”關係。這種關係通常是成對發生的,可以叫作“對聯”(Couplet)——比如集合和迭代器(Enumeration)。前面的Trash-Visitor對似乎也是這樣的一種“對聯”。

16.8 RTTI真的有害嗎

本章的各種設計模式都在努力避免使用RTTI,這或許會給大家留下“RTTI有害”的印象(還記得可憐的goto嗎,由於給人印象不佳,根本就沒有放到Java裡來)。但實際情況並非絕對如此。正確地說,應該是RTTI使用不當才“有害”。我們之所以想避免RTTI的使用,是由於它的錯誤運用會造成擴展性受到損害。而我們事前提出的目標就是能向系統自由加入新類型,同時保證對周圍的代碼造成儘可能小的影響。由於RTTI常被濫用(讓它查找系統中的每一種類型),會造成代碼的擴展能力大打折扣——添加一種新類型時,必須找出使用了RTTI的所有代碼。即使僅遺漏了其中的一個,也不能從編譯器那裡得到任何幫助。

然而,RTTI本身並不會自動產生非擴展性的代碼。讓我們再來看一看前面提到的垃圾回收例子。這一次準備引入一種新工具,我把它叫作TypeMap。其中包含了一個Hashtable(散列表),其中容納了多個Vector,但接口非常簡單:可以添加(add())一個新對象,可以獲得(get())一個Vector,其中包含了屬於某種特定類型的所有對象。對於這個包含的散列表,它的關鍵在於對應的Vector裡的類型。這種設計模式的優點(根據Larry O'Brien的建議)是在遇到一種新類型的時候,TypeMap會動態加入一種新類型。所以不管什麼時候,只要將一種新類型加入系統(即使在運行期間添加),它也會正確無誤地得以接受。

我們的例子同樣建立在c16.Trash這個“包”(Package)內的Trash類型結構的基礎上(而且那兒使用的Trash.dat文件可以照搬到這裡來)。

//: DynaTrash.java
// Using a Hashtable of Vectors and RTTI
// to automatically sort trash into
// vectors. This solution, despite the
// use of RTTI, is extensible.
package c16.dynatrash;
import c16.trash.*;
import java.util.*;

// Generic TypeMap works in any situation:
class TypeMap {
  private Hashtable t = new Hashtable();
  public void add(Object o) {
    Class type = o.getClass();
    if(t.containsKey(type))
      ((Vector)t.get(type)).addElement(o);
    else {
      Vector v = new Vector();
      v.addElement(o);
      t.put(type,v);
    }
  }
  public Vector get(Class type) {
    return (Vector)t.get(type);
  }
  public Enumeration keys() { return t.keys(); }
  // Returns handle to adapter class to allow
  // callbacks from ParseTrash.fillBin():
  public Fillable filler() {
    // Anonymous inner class:
    return new Fillable() {
      public void addTrash(Trash t) { add(t); }
    };
  }
}

public class DynaTrash {
  public static void main(String[] args) {
    TypeMap bin = new TypeMap();
    ParseTrash.fillBin("Trash.dat",bin.filler());
    Enumeration keys = bin.keys();
    while(keys.hasMoreElements())
      Trash.sumValue(
        bin.get((Class)keys.nextElement()));
  }
} ///:~

儘管功能很強,但對TypeMap的定義是非常簡單的。它只是包含了一個散列表,同時add()負擔了大部分的工作。添加一個新類型時,那種類型的Class對象的引用會被提取出來。隨後,利用這個引用判斷容納了那類對象的一個Vector是否已存在於散列表中。如答案是肯定的,就提取出那個Vector,並將對象加入其中;反之,就將Class對象及新Vector作為一個“鍵-值”對加入。

利用keys(),可以得到對所有Class對象的一個“枚舉”(Enumeration),而且可用get(),可通過Class對象獲取對應的Vector

filler()方法非常有趣,因為它利用了ParseTrash.fillBin()的設計——不僅能嘗試填充一個Vector,也能用它的addTrash()方法試著填充實現了Fillable(可填充)接口的任何東西。filter()需要做的全部事情就是將一個引用返回給實現了Fillable的一個接口,然後將這個引用作為參數傳遞給fillBin(),就象下面這樣:

ParseTrash.fillBin("Trash.dat", bin.filler());

為產生這個引用,我們採用了一個“匿名內部類”(已在第7章講述)。由於根本不需要用一個已命名的類來實現Fillable,只需要屬於那個類的一個對象的引用即可,所以這裡使用匿名內部類是非常恰當的。

對這個設計,要注意的一個地方是儘管沒有設計成對歸類加以控制,但在fillBin()每次進行歸類的時候,都會將一個Trash對象插入bin

通過前面那些例子的學習,DynaTrash類的大多數部分都應當非常熟悉了。這一次,我們不再將新的Trash對象置入類型Vector的一個bin內。由於bin的類型為TypeMap,所以將垃圾(Trash)丟進垃圾筒(Bin)的時候,TypeMap的內部歸類機制會立即進行適當的分類。在TypeMap裡遍歷並對每個獨立的Vector進行操作,這是一件相當簡單的事情:

    Enumeration keys = bin.keys();
    while(keys.hasMoreElements())
      Trash.sumValue(
        bin.get((Class)keys.nextElement()));

就象大家看到的那樣,新類型向系統的加入根本不會影響到這些代碼,亦不會影響TypeMap中的代碼。這顯然是解決問題最圓滿的方案。儘管它確實嚴重依賴RTTI,但請注意散列表中的每個鍵-值對都只查找一種類型。除此以外,在我們增加一種新類型的時候,不會陷入“忘記”向系統加入正確代碼的尷尬境地,因為根本就沒有什麼代碼需要添加。

16.9 總結

從表面看,由於象TrashVisitor.java這樣的設計包含了比早期設計數量更多的代碼,所以會留下效率不高的印象。試圖用各種設計模式達到什麼目的應該是我們考慮的重點。設計模式特別適合“將發生變化的東西與保持不變的東西隔離開”。而“發生變化的東西”可以代表許多種變化。之所以發生變化,可能是由於程序進入一個新環境,或者由於當前環境的一些東西發生了變化(例如“用戶希望在屏幕上當前顯示的圖示中添加一種新的幾何形狀”)。或者就象本章描述的那樣,變化可能是對代碼主體的不斷改進。儘管廢品分類以前的例子強調了新型Trash向系統的加入,但TrashVisitor.java允許我們方便地添加新功能,同時不會對Trash結構造成幹擾。TrashVisitor.java裡確實多出了許多代碼,但在Visitor裡添加新功能只需要極小的代價。如果經常都要進行此類活動,那麼多一些代碼也是值得的。

變化序列的發現並非一件平常事;在程序的初始設計出臺以前,那些分析家一般不可能預測到這種變化。除非進入項目設計的後期,否則一些必要的信息是不會顯露出來的:有時只有進入設計或最終實現階段,才能體會到對自己系統一個更深入或更不易察覺需要。添加新類型時(這是“回收”例子最主要的一個重點),可能會意識到只有自己進入維護階段,而且開始擴充系統時,才需要一個特定的繼承結構。

通過設計模式的學習,大家可體會到最重要的一件事情就是本書一直宣揚的一個觀點——多態性是OOP(面向對象程序設計)的全部——已發生了徹底的改變。換句話說,很難“獲得”多態性;而一旦獲得,就需要嘗試將自己的所有設計都轉換到一個特定的模子裡去。

設計模式要表明的觀點是“OOP並不僅僅同多態性有關”。應當與OOP有關的是“將發生變化的東西同保持不變的東西分隔開來”。多態性是達到這一目的的特別重要的手段。而且假如編程語言直接支持多態性,那麼它就顯得尤其有用(由於直接支持,所以不必自己動手編寫,從而節省大量的精力和時間)。但設計模式向我們揭示的卻是達到基本目標的另一些常規途徑。而且一旦熟悉並掌握了它的用法,就會發現自己可以做出更有創新性的設計。

由於《設計模式》這本書對程序員造成了如此重要的影響,所以他們紛紛開始尋找其他模式。隨著的時間的推移,這類模式必然會越來越多。JimCoplien(http://www.bell-labs.com/~cope 主頁作者)向我們推薦了這樣的一些站點,上面有許多很有價值的模式說明:

http://st-www.cs.uiuc.edu/users/patterns

http://c2.com/cgi/wiki

http://c2.com/ppr

http://www.bell-labs.com/people/cope/Patterns/Process/index.html

http://www.bell-labs.com/cgi-user/OrgPatterns/OrgPatterns

http://st-www.cs.uiuc.edu/cgi-bin/wikic/wikic

http://www.cs.wustl.edu/~schmidt/patterns.html

http://www.espinc.com/patterns/overview.html

同時請留意每年都要召開一屆權威性的設計模式會議,名為PLOP。會議會出版許多學術論文,第三屆已在1997年底召開過了,會議所有資料均由Addison-Wesley出版。

16.10 練習

(1) 將SingletonPattern.java作為起點,創建一個類,用它管理自己固定數量的對象。

(2) 為TrashVisitor.java添加一個名為Plastic(塑料)的類。

(3) 為DynaTrash.java同樣添加一個Plastic(塑料)類。