
一、簡述
訪問者模式是一種將數(shù)據(jù)操作和數(shù)據(jù)結(jié)構(gòu)分離的設(shè)計模式,是23種設(shè)計模式中非常復(fù)雜的一種,而且使用頻率并不高。
定義:封裝一些作用于某種數(shù)據(jù)結(jié)構(gòu)中的各元素的操作(訪問),可以在不改變這個數(shù)據(jù)的前提下定義作用于這些元素的新操作。
顧名思義,某些不能改變的數(shù)據(jù),對于不同的訪問者有不同的訪問(或者操作),為不同的訪問者提供相對應(yīng)的操作。例如:公司CEO就能看到公司所有的真實財報數(shù)據(jù),而作為一個員工可能就只能知道同比去年的增長比例。

- Visitor:訪問者抽象類(或者接口),它定義了對每一個元素(Element)訪問的行為,它的參數(shù)就是可以訪問的元素;理論上,它的方法個數(shù)與元素個數(shù)是一樣的,因此,訪問者模式要求元素的類族要穩(wěn)定,不能頻繁的添加、移除元素。如果出現(xiàn)頻繁修改Visitor接口的情況,說明可能并不適合使用訪問者模式。
- ConcreteVisitor:具體的訪問者,需要實現(xiàn)每一個元素類訪問時所產(chǎn)生的具體行為。
- Element:元素接口(或抽象類),它定義了一個接收訪問者的方法(
accept()方法),意義在于每一個元素都要刻意被訪問者訪問。 - ElementA、ElementB:具體的元素類,提供接受訪問方法的具體實現(xiàn),而這個具體的實現(xiàn),通常情況下是使用訪問者提供的訪問該元素類的方法。
- ObjectStructure:定義當(dāng)中所提到的對象結(jié)構(gòu),對象結(jié)構(gòu)是一個抽象表述,它內(nèi)部管理了元素集合,并且可以迭代這些元素共訪問者訪問。
訪問者模式的最大優(yōu)點就是增加訪問者非常容易,新創(chuàng)建一個實現(xiàn)了Visitor接口的類,然后實現(xiàn)兩個visit()方法對不同的元素進行不同的操作,從而達到數(shù)據(jù)與數(shù)據(jù)操作分離的目的。如果不實用訪問者模式,必定需要使用if-else和類型轉(zhuǎn)換,這便是代碼的維護難度升級了。由此可以看出訪問者模式的作用。
PS:訪問者模式違反了迪米特原則(對訪問者公布元素細節(jié))以及依賴倒置原則(依賴了具體類,沒有依賴抽象),由此可見,此模式需要應(yīng)用在特定的情況中。
二、案例實現(xiàn)
這里就以公司為例,公司員工暫且分為開發(fā)人員和運營人員,而公司的CEO和CTO對于不同員工的KPI關(guān)注點不同,因此我們需要做出不同的處理,接著看看代碼實現(xiàn)
員工基類
很簡單,名字初始化和一個抽象的accept()方法
public abstract class Staff {
public String name;
public int kpi;
public Staff(String name){
this.name = name;
kpi = new Random().nextInt(10);
}
//接受Visitor訪問
public abstract void accept(Visitor visitor);
}
具體員工類
具體的員工,根據(jù)各自不同的職責(zé)添加了不同的方法,開發(fā)人員的KPI和代碼產(chǎn)量相關(guān),于是添加了獲取代碼行數(shù)的方法,而運營人員的KPI和新增用戶量相關(guān),于是添加了獲取新增用戶數(shù)的方法。
/**
* 開發(fā)人員
*/
public class Developer extends Staff {
public Developer(String name) {
super(name);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
//代碼量
public int getCodeLines(){
return new Random().nextInt(10 * 1000);
}
}
/**
* 運營人員
*/
public class Operator extends Staff {
public Operator(String name) {
super(name);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
//新增用戶數(shù)
public int getNewUserNum(){
return new Random().nextInt(10 * 10000);
}
}
訪問者
接下來看看訪問者類的定義
public interface Visitor {
//訪問開發(fā)人員
public void visit(Developer developer);
//訪問運營人員
public void visit(Operator operator);
}
這里可以看到,直接從方法上就區(qū)分Developer和Operator,這里主要考慮到的是,如果使用基類Staff作為參數(shù)的話代碼就會是這個樣子
public void visit(Staff staff){
if(staff instanceof Developer){
Developer developer = (Developer)staff;
System.out.print("開發(fā)" + developer.name
+ ",KPI:" + developer.kpi + ",代碼" + developer.getCodeLines() + "行");
}else if(staff instanceof Operator){
Operator operator = (Operator) staff;
System.out.print("運營" + operator.name + ",KPI:" + operator.kpi);
}
}
可以看到,在visit()方法中,我們就需要判斷參數(shù)的類型以及類型強制轉(zhuǎn)換,這樣的代碼難以擴展和維護。
這是訪問者模式的一個優(yōu)點,也是一個缺點,優(yōu)點在于代碼清晰,某種程度上代碼的維護和擴張更好;而缺點也是一樣,如果需要添加一類
Staff,所有的Visitor都需要在實現(xiàn)一個新的visit()方法。
接下來是具體的訪問者代碼,這里設(shè)定CTO更加關(guān)注開發(fā)人員,CEO更加關(guān)注運營人員。
public class CTOVisitor implements Visitor {
@Override
public void visit(Developer developer) {
System.out.print("開發(fā)" + developer.name
+ ",KPI:" + developer.kpi + ",代碼" + developer.getCodeLines() + "行");
}
@Override
public void visit(Operator operator) {
System.out.print("運營" + operator.name + ",KPI:" + operator.kpi);
}
}
public class CEOVisitor implements Visitor {
@Override
public void visit(Developer developer) {
System.out.print("開發(fā)" + developer.name + ",KPI:" + developer.kpi);
}
@Override
public void visit(Operator operator) {
System.out.print("運營" + operator.name
+ ",KPI:" + operator.kpi + "新增用戶:" + operator.getNewUserNum());
}
}
對象結(jié)構(gòu)
這里的對象結(jié)構(gòu),直接就設(shè)定成了公司,集合就是員工們
public class Company {
private List<Staff> staffList = new ArrayList<>();
public void action(Visitor visitor){
for(Staff staff:staffList){
staff.accept(visitor);
}
}
/**
*
* @param staff
*/
public void addStaff(Staff staff){
staffList.add(staff);
}
}
客戶端代碼
public class Client {
public static void main(String[] agrs){
Company company = new Company();
company.addStaff(new Developer("Bruce Wayne"));
company.addStaff(new Developer("ClarkKent"));
company.addStaff(new Developer("Barry Allen"));
company.addStaff(new Operator("Diana Prince"));
company.addStaff(new Operator("Oliver Queen"));
company.addStaff(new Operator("Dinah Lance"));
CEOVisitor ceo = new CEOVisitor();
company.action(ceo);
CTOVisitor cto = new CTOVisitor();
company.action(cto);
}
}
具體輸出如下:
CEO所看到的======
開發(fā)Bruce Wayne,KPI:6
開發(fā)ClarkKent,KPI:2
開發(fā)Barry Allen,KPI:8
運營Diana Prince,KPI:4,新增用戶:46642
運營Oliver Queen,KPI:1,新增用戶:7687
運營Dinah Lance,KPI:3,新增用戶:67382
CTO所看到的======
開發(fā)Bruce Wayne,KPI:6,代碼8285行
開發(fā)ClarkKent,KPI:2,代碼8351行
開發(fā)Barry Allen,KPI:8,代碼658行
運營Diana Prince,KPI:4
運營Oliver Queen,KPI:1
運營Dinah Lance,KPI:3
三、分派
變量被聲明時的類型叫做變量的靜態(tài)類型(Static Type),靜態(tài)變量類型又可以叫做明顯類型(Apparent Type);而變量所引用的對象的正式類型叫做變量的實際類型(Actual Type)。
List list = new ArrayList();
在Java代碼中很常見的一種寫法,聲明父類對象創(chuàng)建子類對象;聲明是List類型(也就是靜態(tài)類型即明顯類型),創(chuàng)建的是ArrayList的對象(實際類型)。
這里就需要提到一個詞,分派(Dispatch)。當(dāng)使用上述形式聲明并創(chuàng)建對象,根據(jù)對象的類型對方法進行選擇,這就是分派,而分派有可以分為靜態(tài)分派(Static Dispatch)和動態(tài)分派(Dynamic Dispatch)。
- 靜態(tài)分派,對應(yīng)的就是編譯時,根據(jù)靜態(tài)類型信息發(fā)生的分派。方法重載就屬于靜態(tài)分派
- 動態(tài)分派,對應(yīng)的就是運行時,動態(tài)地置換掉某個方法。方法重寫就屬于動態(tài)分派
靜態(tài)分派
簡化三個類之間的關(guān)系
public class Staff {
}
public class Developer extends Staff {
}
public class Operator extends Staff {
}
執(zhí)行類,execute()方法有三個重載方法,方法的參數(shù)分別上面對應(yīng)的三個類型Staff、Developer、Operator的對象。
public class Execute {
public void execute(Staff staff){
System.out.println("員工");
}
public void execute(Developer developer){
System.out.println("開發(fā)人員");
}
public void execute(Operator operator){
System.out.println("運營人員");
}
}
測試代碼以及測試結(jié)果
public class Client {
public static void main(String[] agrs){
System.out.println("運行結(jié)果:");
Staff staff = new Staff();
Staff staff1 = new Developer();
Staff staff2 = new Operator();
Execute execute = new Execute();
execute.execute(staff);
execute.execute(staff1);
execute.execute(staff2);
}
}
運行結(jié)果:
員工
員工
員工
可以推斷出,傳入三個對象,最后執(zhí)行的方法都是參數(shù)類型是Staff的方法,即使三個對象有不同的真實類型
方法重載中實際起作用的是它們靜態(tài)類型,也就是在編譯時期就完成了分派,即靜態(tài)分派。
動態(tài)分派
三個類自帶execute()方法,Developer和Operator繼承Staff,并重寫了execute()方法
public class Staff {
public void execute(){
System.out.println("員工");
}
}
public class Developer extends Staff {
@Override
public void execute() {
System.out.println("開發(fā)人員");
}
}
public class Operator extends Staff {
@Override
public void execute() {
System.out.println("運營人員");
}
}
測試代碼以及結(jié)果
public class Client {
public static void main(String[] agrs) {
System.out.println("運行結(jié)果:");
Staff staff = new Staff();
staff.execute();
Staff staff1 = new Developer();
staff1.execute();
Staff staff2 = new Operator();
staff2.execute();
}
}
運行結(jié)果:
員工
開發(fā)人員
運營人員
測試時的情況相同,三個對象,其靜態(tài)類型都是Staff,而實際類型分別是Staff、Developer和Operator。可以看到重寫execute()方法都生效了,各自輸出了對應(yīng)的內(nèi)容。
Java編譯器在編譯時期并不總是知道哪些代碼會被執(zhí)行,因為編譯器僅僅知道對象的靜態(tài)類型,而不知道對象的真實類型;而方法的調(diào)用則是根據(jù)對象的真實類型,而不是靜態(tài)類型。
單分派與多分派
首先需要了解一個叫宗量的概念。一個方法所屬的對象叫做方法的接收者,方法的接收者與方法的參量統(tǒng)稱做方法的宗量。而根據(jù)分派可以基于多少種宗量,可以將面向?qū)ο蟮恼Z言劃分為單分派語言和多分派語言。
單分派語言根據(jù)一個宗量的類型(真實類型)進行對方法的選擇
多分派語言根據(jù)多個的宗量的類型對方法進行選擇
那Java屬于什么類型呢?
我們可以分析一下,Java中靜態(tài)分派時決定方法的選擇的宗量包括方法的接收者和方法參數(shù)的靜態(tài)類型,所以是多分派;而在動態(tài)分派時,方法的選擇只會考慮方法的接收者的實際類型,所以是單分派。其實Java語言是支持靜態(tài)多分派和動態(tài)單分派的語言。
雙(重)分派
那雙重分派又是什么呢?分派和訪問者模式又有什么關(guān)系呢?接下來就會解釋這些問題
Java支持靜態(tài)多分派和動態(tài)單分派,并不支持動態(tài)多分派;于是就有了兩次單分派組成的雙重分派來替代動態(tài)多分派。而訪問者模式正好就用到了雙重分派的技術(shù)。
雙重分派技術(shù)就是在選擇一個方法的時候,不僅僅要根據(jù)方法的接收者的運行時區(qū)別,還要根據(jù)參數(shù)的運行時區(qū)別(這樣達到兩次分派的效果)。
在訪問者模式中,客戶端將具體的對象傳遞給訪問者,也就是staff.accept(visitor);方法的調(diào)用,完成第一次分派;然后具體的訪問者作為參數(shù)傳入到具體的對象的方法中,也就是這句代碼visitor.visit(this);,將this作為參數(shù)傳遞進去完成第二次分派。雙分派意味著得到的執(zhí)行操作決定于請求的種類和接受者的類型。雙重分派的核心就是this對象。
從訪問者模式可以看出,雙重分派就是在沖在方法委派的前面加上了繼承的重寫,使得從某種角度來說重載變成了動態(tài)。
Android源碼中的訪問者模式
相信注解應(yīng)該不會陌生,現(xiàn)在很多出名框架的使用方式都是使用注解,例如:ButterKnife、Dagger、Retrofit等等,都是以注解的方式使用,已達到簡化代碼或者降低耦合度的目的。而注解又可以分為運行時注解和編譯時注解,運行時注解由于性能問題也一直被人詬病,編譯時注解的核心原理依賴APT(Annotation Processing Tools)實現(xiàn),之前提到的框架也是基于APT實現(xiàn)的。
而對于注解的解析過程就是遵從訪問者模式的,其元素就是包、類、方法、方法參數(shù)等(其實就是可以被添加注解那些元素),對于元素的訪問者支持所有的元素訪問,通過繼承一個抽象的元素訪問者實現(xiàn)針對不同類型進行不同的處理。
注解相關(guān)具體的內(nèi)容我不是很了解,只是簡單的說明一下
四、總結(jié)
訪問者模式把數(shù)據(jù)結(jié)構(gòu)和作用于結(jié)構(gòu)上的操作解耦合,使得操作集合可相對自由地演化。訪問者模式適用于數(shù)據(jù)結(jié)構(gòu)相對穩(wěn)定算法又易變化的系統(tǒng)。因為訪問者模式使得算法操作增加變得容易。若系統(tǒng)數(shù)據(jù)結(jié)構(gòu)對象易于變化,經(jīng)常有新的數(shù)據(jù)對象增加進來,則不適合使用訪問者模式。
優(yōu)點
- 擴展性好: 在不修改對象結(jié)構(gòu)中的元素的情況下,為對象結(jié)構(gòu)中的元素添加新的功能。
- 復(fù)用性好: 通過訪問者來定義整個對象結(jié)構(gòu)通用的功能,從而提高復(fù)用程度。
- 分離無關(guān)行為: 通過訪問者來分離無關(guān)的行為,把相關(guān)的行為封裝在一起,構(gòu)成一個訪問者,這樣每一個訪問者的功能都比較單一。
缺點
- 對象結(jié)構(gòu)變化很困難: 不適用于對象結(jié)構(gòu)中的類經(jīng)常變化的情況,因為對象結(jié)構(gòu)發(fā)生了改變,訪問者的接口和訪問者的實現(xiàn)都要發(fā)生相應(yīng)的改變,代價太高。
- 破壞封裝: 訪問者模式通常需要對象結(jié)構(gòu)開放內(nèi)部數(shù)據(jù)給訪問者和
ObjectStructrue,這破壞了對象的封裝性。