假設你設計一個系統,其中會有一些相似類別,類別中都有某些方法內容相似,但還是需要判斷目前要做事的是哪個類別才能呼叫對應的適當類別。通常遇到這種情情,在 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 List 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();
}
}
}
instanceof 雖然簡單直觀,但一般是不鼔勵這樣做。當類別的種類多的時候 (想象所有汽車零件都有一個類別,每個不同類別都有特定的事要做),這樣的寫法反而會加深程式的複雜度。另外一種寫法就是使用繼承、多型的方式,讓類別盡量抽象化。但假如今天類別是以合成的方式聚集在一起,彼此沒有共同介面時,要如何避免使用 instanceof 呢?這就是接下來要介紹的訪問者模式。先來看一下正式定義及類別圖:
(一樣是英文太爛,不知道怎麼翻譯成中文 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

原来这个就是访问者模式。
回覆刪除