跳到主要內容

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

留言

張貼留言

這個網誌中的熱門文章

整理設計模式

        依據 GOF 的書,可以將經典的設計模式分為以下三類:生成、行為、結構。 生成模式 :牽涉到 將物件實體化 。這類模式都提供一個方法,將客戶從所需要實體化的物件中鬆綁出來。 獨體模式 (Singleton Pattern) 工廠方法模式 (Factory Method Pattern) 抽象工廠模式 (Abstract Factory Pattern) 建立者模式 (Builder Pattern) 原型模式 (Prototype Pattern) 結構模式 :讓你 合成類別或物件到大型的結構 。 裝飾者模式 (Decorator Pattern) 轉接器模式 (Adapter Pattern) 表象模式 (Facade Pattern) 合成模式 (Composite Pattern) 代理人模式 (Proxy Pattern) 橋接模式 (Bridge Pattern) 享元模式 (Flyweight Pattern) 行為模式 :模述 類別和物件如何互動 ,以及 各自的責任 。 策略模式 (Strategy Pattern) 觀察者模式 (Observer Pattern) 命令模式 (Command Pattern) 樣板方法模式 (Template Method Pattern) 反覆器模式 (Iterator Pattern) 狀態模式 (State Pattern) 責任鏈模式 (Chain of Responsibility Pattern) 解譯器模式 (Interpreter Pattern) 中介者模式 (Mediator Pattern) 備忘錄模式 (Memento Pattern) 訪問者模式 (Visitor Pattern)         有人可能會覺得裝飾者模式明明有替物件增加行為,為什麼不算是行為模式呢?我們可以從上面的結構模式得知, 結構模式用來描述類別或物件如何被合成,以建立新的結構或功能 。裝飾者模式允許你透過「 將某物件包裝進另一個物件的方式 」,將物件合成以提供新功能,因此焦點應該放在「 動態合成物件,以取得某功能 」,而不是物件之間的溝通。         設入淺出設計模式也有提到一些使用設計模式的

裝飾者模式 (Decorator Pattern)

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