設計模式最多只能對快速集中到一個項目的簡單設計提供很大幫助。但是,在一個特定環境中實現一種設計模式的最簡單方法並不一定是顯而易見的 ― 要做到這一點,有許多種方法。這個月,我們將討論應用公共設計模式來產生簡單、簡短且健壯的代碼的一些方法。
首先,讓我們看一下兩種模式,它們適用於許多不同的環境。在 設計模式(Erich Gamma 等,也稱為“四從組(Gang of Four)”著,它介紹了該主題的一些基礎知識(請參閱 參考資料))中討論的所有設計模式中,我發現可最廣泛應用的是 Composite 和 Visitor 模式。
讓我們進一步研究這兩種模式。
用 Composite 指定遞歸數據類型
很容易就可以看出為什麼 Composite 模式是有用的。Composite 模式只是指定遞歸定義的數據類型的面向對象方法;這些機會始終出現在軟件開發中。
研究遞歸定義的對象類型的能力是軟件工程的最顯著的特征之一(與數字設計成對比,系統是從有限狀態的機器構建的)。
用 Visitor 擴展類層次結構
至於 Visitor 模式,它受到如此廣泛應用的許多原因是它補充了 Composite 模式。
Visitor 常常被吹捧為無需實際修改現有復合類層次結構中的類就可擴展其功能的一種方法。但是,它們的能力遠遠不僅於此。
因為訪問器允許您將一個類層次結構的某些功能與類本身分開,所以可以在各式各樣的設置中使用它們,在這些設置中,從概念上很難將功能看作是那些類的固有部分。
這種現象常常出現在復合數據結構上的向下遞歸中。例如,考慮二叉樹的類層次結構和確定樹中的任何節點是否包含一個零的訪問器:
清單 1. 帶訪問器的二叉樹
abstract class Tree {
public abstract Boolean accept(TreeVisitor that);
}
class Leaf extends Tree {
public static final Leaf ONLY = new Leaf();
public Boolean accept(TreeVisitor that) {
return that.forLeaf(this);
}
}
class Branch extends Tree {
public int value;
public Tree left;
public Tree right;
public Branch(int _value, Tree _left, Tree _right) {
this.value = _value;
this.left = _left;
this.right = _right;
}
public Boolean accept(TreeVisitor that) {
return that.forBranch(this);
}
}
interface TreeVisitor {
public Boolean forLeaf(Leaf that);
public Boolean forBranch(Branch that);
}
class ZeroFinder implements TreeVisitor {
public Boolean forLeaf(Leaf that) {
return new Boolean(false);
}
public Boolean forBranch(Branch that) {
if (that.value == 0) { return new Boolean(true); }
boolean foundInLeft = that.left.accept(this).booleanValue();
boolean foundInRight = that.right.accept(this).booleanValue();
return new Boolean(foundInLeft || foundInRight);
}
}
通過在 for*() 方法(“*”表示通配符,如“Leaf”或“Branch”)中保留這個零查找功能,把 Tree 類本身分開,我們獲得這些優勢:
我們防止了這些類的代碼膨脹。如果每次需要一個新功能時我們就將新的方法添加到類中,那麼它們將很快變得很大。
我們將所有零查找功能放在一個地方,使之便於維護。
訪問器的缺點
現在,如“四人組”所提到的那樣,使用訪問器的主要缺點是將新類添加到復合層次結構中變得有點困難。也就是說,當不期望更改復合層次結構時,使用訪問器是最佳的選擇(雖然對於它還有一些方法可用,如“Extensible Visitor 模式”)。
然而,還有另一個大的缺點。當以同一種方法處理復合層次結構中的許多類時,訪問器中的代碼將比放置在類本身中的代碼大許多(因為我們可能只將一個缺省實現放入父類)。
幸好,普通訪問器有一個變體,它不僅解決了這個問題,而且在許多情況下可以讓我們使代碼比它在放入個別類時更加簡單。我們將這個變體稱為 Depth-First Visitor 模式。
Depth-first visitor 模式
Depth-First Visitor 模式的基本思想如下:大多數訪問器都以深度優先的方式向下遞歸一個數據結構。這意味著它們在訪問給定的(非葉)節點本身之前,先訪問它的子節點。
因此,我們可以在抽象 visitor 類的 for* 方法中實現深度優先遍歷,然後讓具體實現簡單地描述每種數據類型如何組合訪問子節點的結果。這些描述放置在特殊的 for*Only() 方法中,這些方法將訪問其子節點的結果作為方法的參數。
例如,清單 2 演示了如何重寫二叉樹上的深度優先訪問器:
清單 2. 深度優先訪問器
abstract class DepthFirstTreeVisitor implements TreeVisitor {
public abstract Boolean forLeafOnly(Leaf that);
public abstract Boolean forBranchOnly(Branch that,
Boolean left_result,
Boolean right_result);
public Boolean forLeaf(Leaf that) {
return forLeafOnly(that);
}
public Boolean forBranch(Branch that) {
Boolean left_result = that.left.accept(this);
Boolean right_result = that.right.accept(this);
return forBranchOnly(that, left_result, right_result);
}
}
現在,我們可以如清單 3 中所示的那樣重寫 ZeroFinder 訪問器。
清單 3. 重新訪問的 ZeroFinder
abstract class DepthFirstTreeVisitor implements TreeVisitor {
class DepthFirstZeroFinder extends DepthFirstTreeVisitor {
public Boolean forLeafOnly(Leaf that) {
return new Boolean(false);
}
public Boolean forBranchOnly(Branch that,
Boolean left_result,
Boolean right_result)
{
if (that.value == 0) { return new Boolean(true); }
return new Boolean(left_result.booleanValue() ||
right_result.booleanValue());
}
}
這樣,我們已經使許多深度優先遍歷的復雜技術與具體的訪問器分離,並將這些復雜的技術放入(共享的)抽象父類。另外,對於每種類型,深度優先訪問器的實現將有一個容易獲得(在 for*Only() 方法的簽名中)的包含了所有組件的列表。
復雜的層次結構如何?
這實際上相當於我們可以為這個簡單的復合層次結構做些什麼。但是,對於更復雜的類層次結構,我們可以做更多的事情。
通常,對於許多類的復合層次結構,一個給定的訪問器能以一個公共(或通用)方法來處理許多情況。當訪問器代替一對 instanceof 檢查時,這尤為適用。對於這些情況,我們可以提供 for*Only() 方法的缺省實現,這些方法只是調用一個抽象的 defaultCase() 方法:
清單 4. 添加了一個缺省情況
abstract class DepthFirstTreeVisitor implements TreeVisitor {
public abstract Boolean defaultCase(Tree that);
public Boolean forLeafOnly(Leaf that) {
return defaultCase(that);
}
public Boolean forBranchOnly(Branch that,
Boolean left_result,
Boolean right_result)
{
return defaultCase(that);
}
...
}
現在,當在這樣一個復合上實現深度優先訪問器時,我們只需要為那些缺省情況下不處理的情況指定代碼。
當復合層次結構的深度大於一層時,我們還可能需要為該層次結構的每個子樹提供單獨的缺省方法。可以將這些缺省方法定義成簡單地調用父類的缺省方法。但在必要時,可以覆蓋它們。
一旦完成了這些操作,我們就獲得了使功能包括在復合類中的簡潔性,同時保留了普通訪問器的所有優點。
當然,也有警告
對於大多數向下深度優先的訪問器,我們仍可以使用深度優先訪問器。只要覆蓋那些我們不想遍歷深度優先的 for*() 方法(與 for*Only() 方法相對)。
然而,這種方法也有一個警告:很容易遇到 中斷的分派。還記得它們嗎?當我們意外重載一個方法而不是覆蓋父類中的它的實現時,會發生 Broken Dispatch 錯誤模式。通過深度優先訪問器,特別容易發生這種情況。
假設我們想要覆蓋父類的一個 for*() (而不是 for*Only() )方法之一。這個 for*() 方法說明將采用它所操作的節點,但和 for*Only() 方法不同,它不接受訪問子節點的結果。現在,因為我們正在編寫 for*() 和 for*Only() 方法,所以不難想象我們會遺漏且意外地將 for*Only() 寫成一個 for*() 方法的名稱。
如果我們這樣做了,代碼將正常編譯,但我們將無法覆蓋 for*() 方法。相反,我們可用不同說明的新方法使 for*Only() 方法重載。
另外,永遠不要調用這個新的 for*Only() 方法。類似這樣的中斷分派可能是隱藏特別深的錯誤,需要深入跟蹤 — 類型檢查器捕捉不到它們並且您決不知道它們何時或者是否真正發出了一個錯誤信號。在最壞的情況下,中斷的分派將只以這樣一種方式(其結果看似十分合理,但實際上是絕對錯誤的)修改程序的行為。
關於防止這些問題,我只能說它們是最好的示例,說明了為什麼在編寫代碼之前(或同時)編寫單元測試會節省時間,以及為什麼應該經常運行那些測試!
因此,您就知道了為什麼深度優先訪問器很酷以及它們會怎樣刺痛您。我要感謝我們研究實驗室的研究生 Brian Stoler,他首先向我介紹了這種模式。深度優先訪問器是通過“Java 樹構建器(Java Tree Builder)”構造的,它是一種可以從 參考資料中免費獲得的工具,用於根據 JavaCC 語法自動生成一個復合的層次結構(加上訪問器)。