透明箱擴展性:源代碼可查看不可修改,介於黑箱(不看代碼)與開放箱(直接改代碼)之間,或許是擴展系統最有效、最安全的方法。本文詳述透明箱擴展性兩大維度:資料擴展與功能擴展。
面向對象語言天然支援資料擴展。例:二元樹類別層次:
abstract class Tree {}
class Branch extends Tree { int value; Tree left, right; ... }
class Leaf extends Tree {}
新增非空葉節點只需定義 NonEmptyLeaf extends Tree,無需動既有代碼。
若需易於擴展功能而非子類型,採用訪問者模式。為復合資料型別定義抽象訪問者:
interface TreeVisitor {
Object forBranch(Branch that);
Object forLeaf(Leaf that);
}
每個子類型含 accept(TreeVisitor) 調用對應 forXxx。新增功能(如深度複製)只需實作新 TreeVisitor 子類:
class TreeCopier implements TreeVisitor {
Object forBranch(Branch that) {
return new Branch(that.getValue(), (Tree)that.getLeft().accept(this), (Tree)that.getRight().accept(this));
}
Object forLeaf(Leaf that) { return new Leaf(); }
}
若同時擴展資料子類型(新增 NonEmptyLeaf),現有 TreeVisitor 缺 forNonEmptyLeaf。解決:建立 TreeVisitor2 extends TreeVisitor 加新方法,NonEmptyLeaf.accept 強制轉型調用。但訪問者構造新訪問者時(如美化列印追蹤縮進),子類繼承父類 forBranch 仍構造父類型訪問者,導致運行時轉型失敗:
class TreePrinter2 extends TreePrinter implements TreeVisitor2 {
// 繼承的 forBranch 仍構造 TreePrinter 而非 TreePrinter2!
TreePrinter innerPrinter = new TreePrinter(this.amountOfIndentation + 1, this.out); // 錯誤
}
Krishnamurthi 等人提議:以工廠方法取代構造器建立新訪問者實例,子類覆寫工廠方法回傳正確類型:
// TreePrinter
TreePrinter newTree(int indent, OutputStream out) { return new TreePrinter(indent, out); }
Object forBranch(Branch that) {
TreePrinter innerPrinter = newTree(this.amountOfIndentation + 1, this.out); // 呼叫工廠方法
...
}
// TreePrinter2 覆寫
TreePrinter newTree(int indent, OutputStream out) { return new TreePrinter2(indent, out); }
此即可擴展訪問者模式,同時支援資料與功能雙向擴展。
此設計付出性能代價:遞歸調用昂貴,大型結構易堆疊溢出。JIT 動態內聯與 IBM 尾調用消除緩解此問題。實際上,最需擴展性的部件通常非效能關鍵路徑。下期討論黑箱擴展性。