跳到主要內容

訪問者模式 (Visitor Pattern)

        假設你設計一個系統,其中會有一些相似類別,類別中都有某些方法內容相似,但還是需要判斷目前要做事的是哪個類別才能呼叫對應的適當類別。通常遇到這種情情,在 Java 中最直接的做法就是使用 instanceof 關鍵字來判斷,如以下的簡單範例:
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 模式
        拜訪者模式 Visitor Pattern
        设计模式(20)-Visitor Pattern

留言

張貼留言

這個網誌中的熱門文章

解譯器模式 (Interpreter Pattern)

        解譯器模式簡單來說就是 把一句有特殊規則的語句,透過解釋器將它真正的意思表現出來 。相信有學過 Context-free grammar (CFG),Backus–Naur form (BNF),或是 Compiler 相關程的人會比較了解這個模式 (不是我,我早就忘光了…)。不知道上面術語的人,還是可以透過接下來的介紹來稍微了解這個模式。先來看一下這個模式的正式定義及類別圖: 定義一個語言與其文法,使用一個解譯器來表示這個語言的敘述 Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language Context 通常是指待解譯的語句 AbstractExpression 是所有規則都要實作的介面 TerminalExpression 是指無法再展開的規則,算是最小單位的規則 NonterminalExpression 是指可以再展開的規則,可以展開成 NonterninalExpression 和 TerminalExpression 的組合         從類別圖可以看到,語法可能可以一直展開,這時就可以用語法樹來表示,而語法樹以程式來表達的話,就可以使用 合成模式 。而通常比較正式的語法會用 BNF 來表示,如下: expression ::= plus | minus | variable | number plus ::= expression expression '+' minus ::= expression expression '-' variable ::= 'a' | 'b' | 'c' | ... | 'z' digit = '0' | '1' | ... | '9' number ::= digit | digit number 而 每個語法都會定義一個類別 ,如 pl...

狀態模式 (State Pattern)

        如果今天你要設計一台如下圖的糖果機,你會怎麼設計呢?         有上過資訊相關課程的人,應該不難從上圖聯想到 狀態圖 ,上圖中每個圓圈都是一個狀態,而每個箭頭就代表狀態的轉換。有了這個概念後,把它轉成程式就不難了: public class CandyMachine { // 以下四個值表示糖果機會用到的狀態 final static int SOLD_OUT = 0; final static int NO_COIN = 1; final static int HAS_COIN = 2; final static int SOLD = 3; // 需要有一個變數來記錄目前的狀態 // 初始設為賣完, 因為一開始機器裡沒糖果 int mState = SOLD_OUT; // 也要有另一個變數記錄目前剩多少顆糖果 int mCount = 0; public CandyMachine(int count) { mCount = count; // 機器內有糖果的話就跳到沒投錢的狀態, // 表示糖果機在等人投錢 if(mCount > 0) { mState = NO_COIN; } } // 當投錢時會執行這個方法 public void insertCoin() { if(mState == HAS_COIN) { System.out.println("You can't insert another coin"); } else if(mState == NO_COIN) { mState == HAS_COIN; System.out.println("You inserted a coin"); } ...