1. 程式人生 > >Visitor Pattern

Visitor Pattern

摘要

設計模式是對設計原則的具體實踐,在編碼過程中我們要牢記設計原則,根據當前需求靈活選用我們要使用的設計模式。Visitor Pattern 是一個不常用的模式,在我看來,visitor pattern 也算是面向物件裡的一種奇技淫巧了。

what

什麼是visitor模式?從Wikipedia 上的定義為:In object-oriented programming and software engineering, the visitor design pattern is a way of separating an algorithm from an object structure on which it operates.

. 是將演算法與類結構分開的這麼一種模式,而且是面向物件獨有的。

When

我們在什麼條件下才可以使用這個模式呢?有以下幾個方面需要滿足:

  • 有一組提前定義好的類(已知)
  • 物件上的操作是未知的
  • 物件需要根據不同的類做出不同的操作,因此需要使用(instanceof)判斷

操作就是定義中的algorithm,提前定義好的類就是object structure。也就是說由於操作未知,所以將原本屬於類中的操作拿出來,放到 visitor 中。

Why

其實按上面的定義是不是感覺 visitor pattern 違反了將類本身的職責放在類中這個簡單原則呢?在我看來是的,那為何出現了這種反原則的模式並且堂而皇之的成為了24種模式之一呢?其實這是因為語言自身的問題導致的。visitor pattern 的底層本質是 double dispatching

, 而像Java 這種語言只支援single dispatching,而要實現 double dispatching 怎麼辦呢,就只能使用 visitor pattern這種笨拙的模式了。

How

現在我們有兩種動物,定義如下:

public abstract class Animal{}
public class Dog extends Animal{}
public class Cat extends Animal{}

我們需要增加一個叫聲的方法,由於貓狗的叫聲不一樣,我們需要將3個類都修改一下。

public abstract class Animal {
    public abstract void makeSound();
}
public class Dog extends Animal{
    public void makeSound() {
        System.out.println("wang wang");
    }
}
public class Cat extends Animal{
    public void makeSound(){
        System.out.println("miao miao");
    }
}

過了兩天我們發現還需要增加一個eat 的方法,三個類又需要同步修改,違反了OCP原則,這時我們就可以使用Visitor Pattern,將變化提取出來, Animal的三個類保持不動。

public abstract class Animal{}
public class Dog extends Animal{}
public class Cat extends Animal{}
public abstract class AnimalVisitor {
    public abstract void visit(Animal animal);
    public abstract void visit(Dog dog);
    public abstract void visit(Cat cat);
}
public class MakeSoundVisitor extends AnimalVisitor{
    public void visit(Animal animal){
        System.out.println("animal sound")
    }
    public void visit(Dog dog){
        System.out.println("wang wang");
    }
    public void visit(Cat cat) {
        System.out.println("miao miao");
    }
}
public class EatVisitor extends AnimalVisitor {
    public void visit(Animal animal){
        System.out.println("animal eat");
    }
    public void visit(Dog dog){
        System.out.println("eat dog food");
    }
    public void visit(Cat cat){
        System.out.println("eat cat food");
    }
}

很美好,我們每當有什麼操作需要新增的時候就新增一個繼承了AnimalVisitor的類,由它來定義具體的行為。我們可以這樣使用

Dog dog = new Dog();
Cat cat = new Cat();

EatVisitor eat = new EatVisitor();
eat.visit(dog);
eat.visit(cat);

但這存在一個問題,我們只能使用具體的Dog Cat 類,而不能使用父類Animal

Animal dog = new Dog();
eat.visit(dog);//animal eat

明明dog 是 Dog型別,呼叫卻到了Animal上,就是因為Java不具備按執行時型別來做不同的函式呼叫,也就是上面所說的不支援double dispatching,才導致了這樣的結果。

如何解決這個問題呢?Java是支援方法的動態呼叫的(single dispatching),我們可以根據這個來間接實現double dispatching,也就是由Animal 類族做一次轉發。

public abstract class Animal{
    public void visit(AnimalVisitor visitor){
        visitor.visit(this);
    }
}
public class Dog extends Animal{
    public void visit(AnimalVisitor visitor){
        visitor.visit(this);
    }
}
public class Cat extends Animal{
    public void visit(AnimalVisitor visitor){
        visitor.visit(this);
    }
}

很多visitor 模式的例子對不同的類使用了不同的方法名,這裡我們在每個子類中的程式碼與父類中的程式碼一樣,但是this的含義是不一樣的。

使用時我們就需要由Visitor 呼叫 Animal變成Animal 呼叫Visitor了。

EatVisitor eat = new EatVisitor();
Animal dog = new Dog();
dog.visit(eat);

compare

Visitor Patter 將子類中的實現全部拿到了Visitor中來做,如果熟悉函數語言程式設計的人就會覺得這個模式很面熟。其實這就是函數語言程式設計中強大的模式匹配的一部分,根據不同的子類選擇的不同的函式而已。

summary

Visitor 模式使用的很稀少,很大一個原因是它的條件太苛刻,它要求被visit的類族是穩定的,但是做過需求的都知道類是不斷變化的。網上也有很多人說visitor 模式是一個反模式,不應該使用。我們辯證地來看待,畢竟這是在語言限制的條件做出的不得已選擇。

reference