假設你設計一個系統,其中會有一些相似類別,類別中都有某些方法內容相似,但還是需要判斷目前要做事的是哪個類別才能呼叫對應的適當類別。通常遇到這種情情,在 Java 中最直接的做法就是使用 instanceof 關鍵字來判斷,如以下的簡單範例:
定義的意思大概是這樣:將類別的行為和類別切開。以下詳細解釋類別圖的各元件:
设计模式(20)-Visitor Pattern
public interface CarComponent { public void printMessage(); } public class Wheel implements CarComponent { @Override public void printMessage() { System.out.println("This is a wheel"); } // 這是 Wheel 跟 Engine 不同的方法 public void doWheel() { System.out.println("Checking wheel..."); } } public class Engine implements CarComponent { @Override public void printMessage() { System.out.println("This is a engine"); } // 這是 Wheel 跟 Engine 不同的方法 public void doEngine() { System.out.println("Testing this engine..."); } } public class Car { private Listinstanceof 雖然簡單直觀,但一般是不鼔勵這樣做。當類別的種類多的時候 (想象所有汽車零件都有一個類別,每個不同類別都有特定的事要做),這樣的寫法反而會加深程式的複雜度。另外一種寫法就是使用繼承、多型的方式,讓類別盡量抽象化。但假如今天類別是以合成的方式聚集在一起,彼此沒有共同介面時,要如何避免使用 instanceof 呢?這就是接下來要介紹的訪問者模式。先來看一下正式定義及類別圖:mComponents; public Car() { mComponents = new ArrayList<carcomponent>(); } // 有些時候我們還是需要針對不同類別去做不同的事情 public void setComponent(CarComponent component) { mComponents.add(component); if(component instanceof Wheel) { ((Wheel)component).doWheel(); } else { ((Engine)component).doEngine(); } } }
(一樣是英文太爛,不知道怎麼翻譯成中文 Q_Q)
Represent an operation to be performed on elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.
定義的意思大概是這樣:將類別的行為和類別切開。以下詳細解釋類別圖的各元件:
- Visitor:針對要訪問的元件類別定義一個或多個方法 (以圖來看只有兩個 ConcreteElement,因此一般是定義兩個)。
- ConcreteVisitor:可以想成實際上定義了要做什麼事的訪問者。以圖來看,有兩個訪問者,他們都可以訪問到兩個 ConcreteElement,但兩個訪問者要做的事情不同。
- Element:讓元件類別實作的介面,定義了一個以 Visitor 當參數的方法。
- ConcreteElement:實作 Element 的具象類別,就是我們實際要操作的元件。
- ObjectStructure:主要用途是讓訪問者可以尋訪所有元件。以 Java 來說,通常是以 List,Collection 來存元件。
// 元件類別都要實作的介面,目的是取得 visitor, // 將實際上要做的事透過 visitor 委派 public interface CarElement { public void accept(final CarElementVisitor visitor); } // Visitor 介面。因為我們有四個要訪問的元件, // 所以有四個方法 interface CarElementVisitor { void visit(final Body body); void visit(final Car car); void visit(final Engine engine); void visit(final Wheel wheel); } // Body,Car,Engine,Wheel 是我們實際要操作的元件, // 也就是訪問者會訪問的元件 class Body implements CarElement { // accept 這個方法是實作自 CarElement 這個介面的, // 因此是在程式執行期決定要呼叫哪個元件的 accept, // 這也是第一次 method dispatch。另外每一個子 Visitor // 也都有實作 Visitor 介面,因此 visit 也是在程式執行期 // 才決定要呼叫哪個 Visitor 的 visit,這是第二次 method dispatch // visit 代入 this 做為參數,這看起來很像多型, // 其實是 function overloading。 // 另外傳入的 this 是編譯時期就決定好要傳入的是哪個 Element, // 因為以 Body 來說,在編譯時期就能決定好 this 為 Body 實體 public void accept(final CarElementVisitor visitor) { visitor.visit(this); } } class Car implements CarElement { private CarElement[] mElements; public Car() { this.mElements = new CarElement[] { new Wheel("front left"), new Wheel("front right"), new Wheel("back left"), new Wheel("back right"), new Body(), new Engine() }; } public void accept(final CarElementVisitor visitor) { for(final mElements elem : elements) { elem.accept(visitor); } visitor.visit(this); } } class Engine implements CarElement { public void accept(final ICarElementVisitor visitor) { visitor.visit(this); } } class Wheel implements CarElement { private String name; public Wheel(final String name) { this.name = name; } public String getName() { return this.name; } public void accept(final CarElementVisitor visitor) { visitor.visit(this); } } // 具象訪問者。可以依據訪問到的對象不同而產生不同的行為。 // 這邊因為是範例,沒有使用傳入的元件只是單純印出資訊。 // 但從印出的字不同就可以看出不同的 Visitor 可以有不同的用途 class CarElementDoVisitor implements CarElementVisitor { public void visit(final Body body) { System.out.println("Moving my body"); } public void visit(final Car car) { System.out.println("Starting my car"); } public void visit(final Wheel wheel) { System.out.println("Kicking my " + wheel.getName() + " wheel"); } public void visit(final Engine engine) { System.out.println("Starting my engine"); } } class CarElementPrintVisitor implements ICarElementVisitor { public void visit(final Body body) { System.out.println("Visiting body"); } public void visit(final Car car) { System.out.println("Visiting car"); } public void visit(final Engine engine) { System.out.println("Visiting engine"); } public void visit(final Wheel wheel) { System.out.println("Visiting " + wheel.getName() + " wheel"); } } public class VisitorDemo { public static void main(final String[] arguments) { final CarElement car = new Car(); // 基本上會依據不同用途而建立不同的具象 Visitor, // 因此同一個 car 會因為不同的 Visitor 而有 // 不同執行結果 car.accept(new CarElementPrintVisitor()); car.accept(new CarElementDoVisitor()); } }從範例我們可以知道,特定的操作(邏輯行為)可以包裝在訪問者裡,因此使用訪問者模式,我們可以藉由新增具象訪問者來達到新的操作。因為這個模式容易增加新的操作,且不會影響元件類別,也符合開放封閉守則。但也可以發現,一旦我們需要加新的元件或是修改特定元件類別時,整個架構要改動的地方就會很多,因此訪問者模式不適合用在元件需要經常更動的情形。
最後來總結一下訪問者模式的優缺點:
- 增加新操作容易:如同上面的說明,只要新增一個具象訪問者即可。
- 操作有關的程式碼都集中起來:從範例可看出都是集中在訪問者裡,而不是分散在各個元件類別裡。
- 搭配使用合成模式,可以尋訪不同結構的元素:簡單說就如同 Car 類別,今天 Car 的內部零件分別實作不同介面的話,透過訪問者模式,還是可以所有的零件都呼叫 accept。
- 新增或修改元件類別困難:如同上面的說明,要改動的地方會變很多。
- 破壞類別的封裝:因為訪問者模式是透過訪問者去調用實際元件類別的方法,這樣在某種程度上元件類別需要暴露資訊給訪問者。而假如有更複雜的訪問者,需要儲存元件狀態的話,也會變成元件的資訊要放在訪問者裡。因此在深入淺出設計模式一書中才會提到:當你想要為一個合成增加新的能力,且封裝並不重要時,就使用訪問者模式
參考資料:
深入淺出設計模式
拜訪者模式 Visitor Pattern设计模式(20)-Visitor Pattern
原来这个就是访问者模式。
回覆刪除