跳到主要內容

訪問者模式 (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

留言

張貼留言

這個網誌中的熱門文章

裝飾者模式 (Decorator Pattern)

        假如你有一間飲料店, 目前只有賣幾種咖啡。因為生意很好, 因此想更換菜單…         以下是目前菜單的類別圖:         簡單說明此類別圖, cost() 是抽象的, 子類別要實作自己的 cost() 來告知飲料的價格。         買咖啡時, 也可要求要加料, 例如牛奶(Milk)、摩卡(Mocha,就是巧克力口味)。這樣的新類別要如何設計呢 ? 看起來是不能直接新增所需的子類別, 例如 EspressoWithMilk, EspressoWithMilkAndMocha, DarkRoastWithMilk, DarkRoastWithMilkAndMocha… 這樣加下去, 日後飲料跟配料越來越多時, 類別也就越多, 這實在不是個好設計。         換個方式設計呢, 在 Beverage 裡面加入所有的配料如何 ? 這樣好像也不太好, 未來要是配料有更動, Beverage 程式碼就要重寫, 而未來要是有新口味的飲料時, 有些配料就不太合理 ( 薑茶加摩卡 ? ), 更麻煩的是, 無法應付機車的客人 (例如要加 3 份牛奶)。這時候裝飾者模式就能上場啦。在介紹裝飾者模式前, 先說明其設計守則: 類別應該開放, 以便擴充 ; 應該關閉, 禁止修改。         我們的目標是允許類別容易擴充, 在不修改現有程式碼的情形就能搭配新的行為。這樣的設計具有彈性, 可以接受新功能以達到改變需求的目的。這看起來好像有點矛盾, 但是的確有一些技術可以在不直接修改程式碼的情形下進行擴充, 如裝飾者模式。         這時候應該有人會問: 那是不是以後我的專案架構設計都遵循這個守則就是好設計了 ? 答案是不太可能, 也沒這必要, 就算做得到, 也可能是浪費, 容易導致程式碼複雜且難以理解。只需小心選擇哪些部分未來會擴充, 這些部份遵循這個設計守則即可。         接下來...

狀態模式 (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"); } ...