訪問者模式(Visitor Pattern)。
定義
封裝一些作用於某種資料結構中的各元素的操作,他可以在不改變資料結構的前提下定義作用於這些元素的新的操作。
我們來看幾個角色的職責。
- Visitor——抽象訪問者
抽象類或者介面,宣告訪問者可以訪問哪些元素,具體到程式中就是visit方法的引數定義哪些物件是可以被訪問的。
- ConcreteVisitor——具體訪問者
他影響訪問者訪問到一個類後該怎麼幹,要做什麼事情。
- Element——抽象元素
介面或者抽象類,宣告接受哪一類訪問者訪問,程式上是通過accept方法中的引數來定義的。
- ConcreteElement——具體元素
實現accept方法,通常是visitor.visit(this),基本上都形成了一種模式了。
- ObjectStruture——結構物件
元素產生者,一般容納在多個不同類、不同介面的容器,如List、Set、Map等,在專案中,一般很少抽象出這個角色。 大家可以這樣理解訪問者模式,我作為一個訪客(Visitor)到朋友家(Visited Class)去拜訪,朋友之間聊聊天,喝喝酒,再相互吹捧吹捧,炫耀炫耀,這都正常。聊天的時候,朋友告訴我,他今年加官進爵了,工資也漲了30%,準備再買套房子,那我就在心裡盤算(Visitor-self-method)“你這麼有錢,我去年要借10萬你都不借”,我根據朋友的資訊,執行了自己的一個方法。
通用原始碼
我們來看看訪問者模式的通用原始碼,先看抽象元素,如下所示。
public abstract class Element {
/**
* 定義業務邏輯
*/
public abstract void doSomething();
/**
* 允許誰來訪問
*
* @param visitor
*/
public abstract void accept(IVisitor visitor);
}
抽象元素有兩類方法:一是本身的業務邏輯,也就是元素作為一個業務處理單元必須完成的職責;另外一個是允許哪一個訪問者來訪問。我們來看具體元素,如下所示。
public class ConcreteElement1 extends Element { @Override public void doSomething() { // 業務處理 } @Override public void accept(IVisitor visitor) { visitor.visit(this); } } public class ConcreteElement2 extends Element { @Override public void doSomething() { // 業務處理 } @Override public void accept(IVisitor visitor) { visitor.visit(this); } }
他定義了兩個具體元素,我們再來看抽象訪問者,一般是有幾個具體元素就有幾個訪問方法,如下所示。
public interface IVisitor {
/**
* 訪問element1元素
*
* @param element1
*/
void visit(ConcreteElement1 element1);
/**
* 訪問element2元素
*
* @param element2
*/
void visit(ConcreteElement2 element2);
}
具體訪問者如下所示。
public class Visitor implements IVisitor {
@Override
public void visit(ConcreteElement1 element1) {
element1.doSomething();
}
@Override
public void visit(ConcreteElement2 element2) {
element2.doSomething();
}
}
結構物件是產生出不同的元素物件,我們使用工廠方法模式來模擬,如下所示。
public class ObjectStruture {
/**
* 物件生成器,這裡通過一個工廠方法模式模擬
*
* @return
*/
public static Element createElement() {
Random random = new Random();
if (random.nextInt(100) > 50) {
return new ConcreteElement1();
} else {
return new ConcreteElement2();
}
}
}
進入了訪問者角色後,我們對所有的具體元素的訪問就非常簡單了,我們通過一個場景類模擬這種情況,如下所示。
public class Client {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
// 獲得元素物件
Element element = ObjectStruture.createElement();
// 接受訪問者訪問
element.accept(new Visitor());
}
}
}
通過增加訪問者,只要是具體元素就非常容易訪問,對元素的遍歷就更加容易了,甭管他是什麼物件,只要他在一個容器中,都可以通過訪問者來訪問,任務集中化。這就是訪問者模式。
優點
- 符合單一職責原則
具體元素角色也就是Employee抽象類的兩個子類負責資料的載入,而Visitor類則負責報表的展現,兩個不同的職責非常明確的分離開來,各自演繹變化。
- 優秀的擴充套件性
由於職責分開,繼續增加對資料的操作是非常快捷的,直接在Visitor中增加一個方法,傳遞資料後進行處理。
- 靈活性非常高
通過訪問者模式,把資料扔給訪問者,由訪問者來進行計算。
缺點
- 具體元素對訪問者公佈細節
訪問者要訪問一個類就必然要求這個類公佈一些方法和資料,也就是說訪問者關注了其他類的內部細節,這時迪米特法則所不建議的。
- 具體元素變更比較困難
具體元素角色的增加、刪除、修改都是比較困難的。
- 違背了依賴倒置原則
訪問者依賴的是具體元素,而不是抽象元素,這破壞了依賴倒置原則,特別是在面向物件的程式設計中,拋棄了對介面的依賴,而直接依賴實現類,擴充套件比較難。
使用場景
- 一個物件結構包含很多類物件,他們有不同的介面,而你想對這些丟想實施一些依賴於其具體類的操作,也就是說用迭代器模式已經不能勝任的情景。
- 需要對一個物件結構中的物件進行很多不同並且不想管的操作,而你想避免讓這些操作“汙染”這些物件的類。
總結一下,在這種地方你一定要考慮使用訪問者模式:業務規則要求遍歷多個不同的物件。這本身也是訪問者模式出發點,迭代器模式只能訪問同類或同介面的資料(當然了,如果你使用instanceof,那麼能訪問所有的資料,這沒有爭論),而訪問者模式是對迭代器模式的擴充,可以遍歷不同的物件,然後執行不同的操作,也就是針對訪問的物件不同,執行不同的操作。訪問者模式還有一個用途,就是充當攔截器角色。
擴充套件
統計功能
多個訪問者
雙分派
說到訪問者模式就不得不提一下雙分派問題,什麼是雙分派呢?我們先來解釋一下什麼是單分派和多分派,單分派語言處理一個操作是根據請求者的名稱和接收到的引數決定的,在Java中有靜態繫結和動態繫結之說,他的實現是依據過載和覆寫實現的。我們來說一個簡單的例子。
例如,演員演電影角色,一個演員可以扮演多個角色,我們先定義一個影視中的兩個角色:功夫主角和白痴配角,如下所示。
public interface Role {
}
public class KungFuRole implements Role {
}
public class IdiotRole implements Role {
}
角色有了,我們再定義一個演員抽象類,如下所示。
public abstract class AbsActor {
/**
* 演員都能夠扮演一個角色
*
* @param role
*/
public void act(Role role) {
System.out.println("演員可以扮演任何角色");
}
/**
* 可以演功夫戲
*
* @param role
*/
public void act(KungFuRole role) {
System.out.println("演員都可以演功夫角色");
}
}
很簡單,這裡使用了Java的過載,我們再來看青年演員和老年演員,採用覆寫的方式來細化抽象類的功能,如下所示。
public class YoungActor extends AbsActor {
@Override
public void act(KungFuRole role) {
System.out.println("最喜歡演功夫角色");
}
}
public class OldActor extends AbsActor {
@Override
public void act(KungFuRole role) {
System.out.println("年齡大了,不能演功夫角色");
}
}
覆寫和過載都已經實現,我們編寫一個場景,如下所示。
public class Client {
public static void main(String[] args) {
// 定義一個演員
AbsActor actor = new OldActor();
// 定義一個角色
Role role = new KungFuRole();
// 開始演戲
actor.act(role);
actor.act(new KungFuRole());
}
}
過載在編譯器就決定了要呼叫哪個方法,他是根據role的表面型別而決定呼叫act(Role role)方法,這時靜態繫結;而Actor的執行方法act則是由其實際型別決定的,這時動態繫結。
一個演員可以扮演很多角色,我們的系統要適用這種變化,也就是根據演員、角色兩個物件型別,完成不同的操作任務,該如何實現呢?很簡單,我們讓訪問者模式上場就可以解決該問題,只要把角色類稍稍修改即可,如下所示。
public interface Role {
/**
* 演員要扮演的角色
*
* @param actor
*/
void accept(AbsActor actor);
}
public class KungFuRole implements Role {
@Override
public void accept(AbsActor actor) {
actor.act(this);
}
}
public class IdiotRole implements Role {
@Override
public void accept(AbsActor actor) {
actor.act(this);
}
}
場景類稍有改動,如下所示。
public class Client {
public static void main(String[] args) {
// 定義一個演員
AbsActor actor = new OldActor();
// 定義一個角色
Role role = new KungFuRole();
// 開始演戲
role.accept(actor);
}
}
不管演員類和角色類怎麼變化,我們都能夠找到期望的方法執行,這就是雙反派。雙反派意味著得到執行的操作決定於請求的種類和兩個接收者的型別,他是多分派的一個特例。從這裡也可以看到Java是一個支援多分派的單分派語言。
最佳實踐
訪問者模式是一種集中規整模式,特別適用於大規模重構的專案,在這一個階段需求已經非常清晰,原系統的功能點也已經明確,通過訪問者模式可以很容易把一些功能進行梳理,達到最終目的——功能集中化,如一個統一的報表運算、UL展現,我們還可以與其他模式混編建立一套自己的過濾器或者攔截器。