Visitor 模式


您的系統中有客戶、會員與VIP,假設經過設計考量,確定以下的設計是必要的:
class Customer {
void doCustomer() {
System.out.println("客戶服務");
}
void pay() {
System.out.println("結帳");
}
}

class Member extends Customer {
void doMember() {
System.out.println("會員服務");
}
}

class VIP extends Customer {
void doVIP() {
System.out.println("VIP 服務");
}
}

您要設計一個結帳功能,針對客戶所使用的服務計算客戶要付的費用,計算的演算大部份是針對Customer來進行操作,但其中幾個步驟,不免要針對特定客 戶類型來設計,例如:
class Service {
void doService(Customer customer) {
customer.doCustomer();
if(customer instanceof Member) {
((Member) customer).doMember();
}
if(customer instanceof VIP) {
((VIP) customer).doVIP();
}
customer.pay();
}
}

使用instanceof來判斷物件類型,一般是不被鼓勵的,如果您的客戶類型繁多,這樣的結構化設計會逐漸加深程式碼的繁複。一般多希望利用多型操作來 解決問題,不要針對特定類型來進行設計。

如果經過仔細的考量設計,必須針對特定類型來進行操作確實是不可避免的,那麼您可以換個方式,例如:
interface Visitable {
void accept(Visitor visitor);
}

interface Visitor {
void visit(Member member);
void visit(VIP vip);
}

class Customer implements Visitable {
void doCustomer() {
System.out.println("客戶服務");
}
void pay() {
System.out.println("結帳");
}
public void accept(Visitor visitor) {
// nothing to do
}
}

class Member extends Customer {
void doMember() {
System.out.println("會員服務");
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this); // 看似多型,其實是 overload
}
}

class VIP extends Customer {
void doVIP() {
System.out.println("VIP 服務");
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this); // 看似多型,其實是 overload
  }

}

class VisitorImpl implements Visitor {
public void visit(Member member) {
member.doMember();
}
public void visit(VIP vip) {
vip.doVIP();
}
}

class Service {
private Visitor visitor = new VisitorImpl();
void doService(Customer customer) {
customer.doCustomer();
((Visitable) customer).accept(visitor);
customer.pay();
}
}

public class Main {
public static void main(String[] args) {
Service service = new Service();
service.doService(new Member());
}
}

doService()方法接受的是Customer型態,原先為了針對特別型態作特定操作,不得已使用instanceof作判斷,而上面這個設計,則 是由Visitor登門入戶,使用物件中的this參考名稱之型態資訊,由物件自行 選擇要呼叫的overload方法。

這是Visitor模式的一個實現,Visitor模式的目的,是將與操作被物件的結構相關的演算分離出來,可以各自針對結構進行個別處理,不同的Visitor可作為不同演算的名稱空間。

靜態語言可以使用overload來實現Visitor模式,overload是編譯時期就決定要呼叫哪個方法,對於不支援overload的語言,或者是 動態語言,則可以在Visitor的方法名稱上作個區別(只是少了單一方法名稱呼叫的方便,其實若您將overload的型態資訊也看成是方法名稱的一部 份,也就是方法簽署,道理就相同了)。例如,以下是Python的實現方式:
class Customer:
def doCustomer(self):
print("客戶服務")

def pay(self):
print("結帳")

def accept(self, visitor): pass

class Member(Customer):
def doMember(self):
print("會員服務")

def accept(self, visitor):
visitor.visitMember(self)

class VIP(Customer):
def doVIP(self):
print("VIP 服務")

def accept(self, visitor):
visitor.visitVIP(self)

class VisitorImpl:
def visitMember(self, member):
member.doMember();

def visitVIP(self, vip):
vip.doVIP()

class Service:
def __init__(self):
self.visitor = VisitorImpl()

def doService(self, customer):
customer.doCustomer()
customer.accept(self.visitor)
customer.pay()

service = Service()
service.doService(VIP())

Visitor模式的 UML 結構類圖如下:

好的!看到這邊,我可以告訴你,以上使用 Visitor 模式的方式是個不好的例子,大部份談 Visitor 模式的文件或書籍,可能都是類似這種舉例方式,有些文件或書籍甚至會跟你說,Visitor 模式是個反模式,因為基本上,以上的需求都可以用子型態多型來解決,Visitor 模式本身不是個反模式,以上使用 Visitor 模式的方式才是反模式。

Visitor 模式裡頭一定要有個 Visitor 嗎?不要被 UML 結構類圖誤導了,要不要有 Visitor 是看你的需求!另一方面,Visitor 模式的本質上,就是 overload 的應用,其真正應用的之一,是在實現 pattern matching 之類的場合,而談到 pattern matching,若要真正認識,建議要瞭解函數式典範,有興趣可以參考〈Java Lambda Tutorial〉中〈Java 開發者的函數式程式設計〉的文件。

其中在〈List 處理模式〉的部份,可以使用 Java 17 實現 pattern matching,範例程式碼可以參考〈代數資料型態:Java 17〉,如果沒有 Java 17 呢?就用 Visitor 模式實現,例如〈代數資料型態:Visitor 實現〉,你會說又不寫函數式,其實 pattern matching 的本質,是讓你能根據「資料載體(data carrier)」的結構,彈性地增加必要的函數。

Visitor 模式會被誤解就是因為大家都搞了個 Visitor,不過,Java 日後在 pattern matching 的特性都完備之後,Visitor 模式的應用場合就會更少了。