訪問者模式 Visitor
《俠客行》是當代作家金庸創作的長篇武俠小說,新版電視劇《俠客行》中,開篇有一段獨白:
“茫茫海外,傳說有座俠客島,島上賞善罰惡二使,每隔十年必到中原武林,向各大門派下發放賞善罰惡令,
強邀掌門人赴島喝臘八粥,拒接令者,皆造屠戮,無一幸免,接令而去者,杳無音訊,生死未仆,俠客島之行,已被視為死亡之旅。”
不過話說電視劇,我總是覺得老版的好看。
意圖
表示一個作用於某對象結構中的各元素的操作。
它使你可以在不改變各元素類的前提下定義作用於這些元素的新操作。
意圖解析
我們以代碼描述一下《俠客行》中的這個場景
假定:
賞善罰惡二使,一個叫做張三,一個叫做李四,面對一眾掌門
張三負責賞善,對好人賞賜,壞人他不處理;
相反,李四負責罰惡,好人不處理,對壞人懲罰
俠客行代碼示例
定義了
“掌門人”接口
package visitor.俠客行; public interface 掌門人 { }
“掌門人”有兩種類型
沒做過壞事的掌門,
做過壞事的掌門
package visitor.俠客行; public class 沒做過壞事的掌門 implements 掌門人 { }
package visitor.俠客行; public class 做過壞事的掌門 implements 掌門人 { }
定義了
俠客島,俠客島管理維護“江湖的掌門人”,使用List
提供了掌門人的添加方法 “add掌門人(掌門人 某掌門)”
定義了“賞善罰惡(String 處理人)”方法,用於賞善罰惡,接受參數為處理人
如果是賞善大使張三,他會賞賜好人,不管壞人
如果是罰惡大使李四,他會懲罰壞人,不管好人
package visitor.俠客行; import java.util.ArrayList; import java.util.List; public class 俠客島 { private List<掌門人> 掌門人List = new ArrayList<>(); public void add掌門人(掌門人 某掌門) { 掌門人List.add(某掌門); } public void 賞善罰惡(String 處理人) { if (處理人.equals("張三")) { for (掌門人 某掌門X : 掌門人List) { if (某掌門X instanceof 沒做過壞事的掌門) { System.out.println("好掌門, 張三: 賞賜沒做過壞事的掌門"); } else if (某掌門X instanceof 做過壞事的掌門) { System.out.println("壞掌門, 張三: 不管做過壞事的掌門"); } System.out.println(); } } else if (處理人.equals("李四")) { for (掌門人 某掌門X : 掌門人List) { if (某掌門X instanceof 沒做過壞事的掌門) { System.out.println("好掌門, 李四: 不管沒做過壞事的掌門"); } else if (某掌門X instanceof 做過壞事的掌門) { System.out.println("壞掌門, 李四: 懲罰做過壞事的掌門"); } System.out.println(); } } } }
測試代碼
上面的測試代碼中,我們創造了俠客島的“賞善罰惡二使”
並且將幾個“掌門人”交於他們處理
打印結果分別展示了對於這幾個“掌門人”,張三和李四的不同來訪,產生的不同結果
如果我們想增加來訪者怎么辦?比如這次是龍木島主親自出島處理,好人賞賜,壞人直接處理怎么辦?
我們可以直接新增賞善罰惡方法的處理邏輯,如下圖所示,新增加了一個else if
可以通過測試代碼看到結果
如果有些掌門人既沒有做什么好事,也沒有做什么壞事怎么處理?也就是新增一種掌門人?
你會發現,所有的判斷的地方,也還是都需要新增加一個else if ...... ̄□ ̄||
因為
上面的示例,使用的是兩層判斷邏輯,每一層都跟具體的類型有關系!!!
不管是增加新的來訪者,還是增加新的種類的成員,都不符合開閉原則,而且判斷邏輯復雜混亂
上面的過程在程序世界中, 也會經常出現。
實際開發中,經常用到集合框架
集合框架中也經常會保存不同的類型(此處指的是不同的最終類型,如果抬杠,還不都是Object  ̄□ ̄||)
比如多個不同的子類,像上面示例中的好掌門和壞掌門,都是掌門人類型,但是具體子類型不同。
對於集合中的元素,可能會有不同的處理操作
比如上面示例中的,張三和李四的到來,處理肯定不一樣,沒干過壞事的和干過壞事的處理也不一樣
比如去體檢,不同的項目的醫生會有不同的行為操作,你和跟你一起排隊體檢的人也不一樣,但是你還是你,他還是他
在上面的《俠客行》的示例中,我們使用了
雙重判斷來確定下面兩層問題:
一層是來訪者是誰? 另外一層是當前的掌門人是什么類型?
如果有X種來訪者,Y種類型掌門人,怕是要搞出來X*Y種組合了,所以才會邏輯復雜,擴展性差
所以,那么
根本問題就是靈活的確定這兩個維度,來訪者和當前類型
,進而確定具體的行為,對吧?
再回頭審視一下《俠客行》的示例,對於訪問者,有張三、李四、龍木島主,還可能會有其他人,
顯然,我們應該
嘗試將訪問者進行抽象,張三,李四,龍木島主,他們都是具體的訪問者。
而且,而且,而且,
他們都會訪問不同類型的掌門人,既然是訪問 不同類型掌門人
也就是方法名一樣,類型不一樣?
這不就是
方法重載么
新版代碼示例
掌門人相關角色不變
package visitor.新版俠客行; public interface 掌門人 { } package visitor.新版俠客行; public class 沒做過壞事的掌門 implements 掌門人 { } package visitor.新版俠客行; public class 做過壞事的掌門 implements 掌門人 { }
新增加訪問者角色,訪問者既可能訪問好人,也可能訪問壞人,使用方法的重載在解決
方法都是拜訪,有兩種類型的重載版本
package visitor.新版俠客行; public interface 訪問使者 { void 拜訪(做過壞事的掌門 壞人); void 拜訪(沒做過壞事的掌門 好人); }
張三負責賞善,當他訪問到好人時,賞賜,壞人不處理
package visitor.新版俠客行; public class 張三 implements 訪問使者 { @Override public void 拜訪(沒做過壞事的掌門 好人) { System.out.println("好掌門, 張三: 賞賜沒做過壞事的掌門"); } @Override public void 拜訪(做過壞事的掌門 壞人) { System.out.println("壞掌門, 張三: 不管做過壞事的掌門"); } }
李四負責罰惡,訪問到好人時不處理,遇到壞人時,就懲罰!
package visitor.新版俠客行; public class 李四 implements 訪問使者 { @Override public void 拜訪(沒做過壞事的掌門 好人) { System.out.println("好掌門, 李四: 不管沒做過壞事的掌門"); } @Override public void 拜訪(做過壞事的掌門 壞人) { System.out.println("壞掌門, 李四: 懲罰做過壞事的掌門"); } }
引入了訪問使者角色,我們就不需要對使者進行判斷了
借助了使者的多態性,不管是何種使者都有訪問不同類型掌門人的方法
所以可以去掉了一層邏輯判斷,代碼簡化如下
package visitor.新版俠客行; import java.util.ArrayList; import java.util.List; public class 俠客島 { private List<掌門人> 掌門人List = new ArrayList<>(); public void add掌門人(掌門人 某掌門) { 掌門人List.add(某掌門); } public void 賞善罰惡(訪問使者 使者) { for (掌門人 某掌門X : 掌門人List) { if (某掌門X instanceof 沒做過壞事的掌門) { 使者.拜訪((沒做過壞事的掌門)某掌門X); } else if (某掌門X instanceof 做過壞事的掌門) { 使者.拜訪((做過壞事的掌門)某掌門X); } System.out.println(); } } }
測試代碼也稍作調整
定義了兩個訪問者,傳遞給“賞善罰惡”方法
package visitor.新版俠客行; public class Test { public static void main(String[] args){ 俠客島 善善罰惡二使 = new 俠客島(); 善善罰惡二使.add掌門人(new 做過壞事的掌門()); 善善罰惡二使.add掌門人(new 沒做過壞事的掌門()); 善善罰惡二使.add掌門人(new 沒做過壞事的掌門()); 善善罰惡二使.add掌門人(new 做過壞事的掌門()); 訪問使者 張三 = new 張三(); 訪問使者 李四 = new 李四(); 善善罰惡二使.賞善罰惡(李四); 善善罰惡二使.賞善罰惡(張三); } }
可以看到,《新版俠客行》和老版本的功能的一樣的,但是代碼簡化了
而且,最重要的是能夠很方便的擴展使者,比如我們仍舊增加“龍木島主”這一訪客。
package visitor.新版俠客行; public class 龍木島主 implements 訪問使者 { @Override public void 拜訪(做過壞事的掌門 壞人) { System.out.println("龍木島主,懲罰壞人"); } @Override public void 拜訪(沒做過壞事的掌門 好人) { System.out.println("龍木島主,賞賜好人"); } }
新增加了"龍木島主“訪客后,客戶端可以直接使用了,不需要修改”俠客島“的代碼了
測試代碼增加如下兩行,查看下面結果
但是如果增加新的掌門人類型呢?
因為我們仍舊有具體類型的判斷,如下圖所示
所以,想要增加新的掌門人,又完蛋了  ̄□ ̄||
而且,現在的判斷邏輯也還是交織着,復雜的。
對於訪問者的判斷,我們借助於多態以及方法的重載,去掉了一層訪問者的判斷
通過多態可以將請求路由到真實的來訪者,通過方法重載,可以調用到正確的方法
如果能把這一層的if else if判斷也去掉,是不是就可以靈活擴展掌門人了呢?
ps:有人覺得“拜訪”方法的類型使用 掌門人 不就好了么
但是對於不同的具體類型有不同的行為,那你在“拜訪”方法中還是少不了要進行判斷,只是此處判斷還是“拜訪”方法內判斷的問題)
前面的那段if else if判斷邏輯,訪問的方法都是
使者.拜訪,只不過具體類型不同
但是如何確定類型?問題也就轉換為”到底怎么判斷某掌門X的類型“或者”到底誰知道某掌門X的類型“
那誰知道他的類型呢?
如果不借助外力,比如 instanceof 判斷的話,還有誰知道?
某掌門X 他自己知道!!!他自己知道!!!
所以,如果是在 某掌門X自己內部的方法,就可以獲取到this了,這就是當前對象的真實類型
把這個類型在回傳給來訪使者不就可以了么
所以
給掌門人定義一個“
接受拜訪”方法,不管何種類型的掌門人,都能夠接受各種訪客的拜訪
接受拜訪(訪問使者 賞善罰惡使者){
賞善罰惡使者.拜訪(this);
最新版俠客行代碼示例
說起來有點迷惑,我看看代碼
《最新版俠客行》
掌門人都增加了”接受拜訪“的方法
package visitor.最新版本俠客行; public interface 掌門人 { void 接受拜訪(訪問使者 賞善使者); }
package visitor.最新版本俠客行; public class 沒做過壞事的掌門 implements 掌門人 { @Override public void 接受拜訪(訪問使者 賞善罰惡使者) { 賞善罰惡使者.拜訪(this); } }
package visitor.最新版本俠客行; public class 做過壞事的掌門 implements 掌門人 { @Override public void 接受拜訪(訪問使者 賞善罰惡使者) { 賞善罰惡使者.拜訪(this); } }
訪問使者相關角色與《新版俠客行》中一樣
package visitor.最新版本俠客行; public interface 訪問使者 { void 拜訪(做過壞事的掌門 壞人); void 拜訪(沒做過壞事的掌門 好人); } package visitor.最新版本俠客行; public class 張三 implements 訪問使者 { @Override public void 拜訪(沒做過壞事的掌門 好人) { System.out.println("好掌門, 張三: 賞賜沒做過壞事的掌門"); } @Override public void 拜訪(做過壞事的掌門 壞人) { System.out.println("壞掌門, 張三: 不管做過壞事的掌門"); } }
package visitor.最新版本俠客行; public class 李四 implements 訪問使者 { @Override public void 拜訪(沒做過壞事的掌門 好人) { System.out.println("好掌門, 李四: 不管沒做過壞事的掌門"); } @Override public void 拜訪(做過壞事的掌門 壞人) { System.out.println("壞掌門, 李四: 懲罰做過壞事的掌門"); } }
此時的俠客島輕松了,不再需要來回的判斷類型了
package visitor.最新版本俠客行; import java.util.ArrayList; import java.util.List; public class 俠客島 { private List<掌門人> 掌門人List = new ArrayList<>(); public void add掌門人(掌門人 某掌門) { 掌門人List.add(某掌門); } public void 賞善罰惡(訪問使者 使者) { for (掌門人 某掌門X : 掌門人List) { 某掌門X.接受拜訪(使者); System.out.println(); } } }
從結果看跟上一個版本一樣
但是很顯然,我們的俠客島輕松了
接下來我們看一下新增加訪客和新增加掌門人的場景
擴展龍木島主
package visitor.最新版本俠客行; public class 龍木島主 implements 訪問使者 { @Override public void 拜訪(做過壞事的掌門 壞人) { System.out.println("龍木島主,懲罰壞人"); } @Override public void 拜訪(沒做過壞事的掌門 好人) { System.out.println("龍木島主,賞賜好人"); } }
測試代碼如下,顯然因為拜訪使者的抽象,才得以能夠更好的擴展訪問者,所以此處肯定跟《新版俠客行》一樣便於擴展
看看如果擴展一個新的掌門人
package visitor.最新版本俠客行; public class 不好不壞的掌門 implements 掌門人 { @Override public void 接受拜訪(訪問使者 賞善罰惡使者) { 賞善罰惡使者.拜訪(this); } }
但是,”訪問使者“里面沒有能夠拜訪”不好不壞的掌門“方法啊?怎么辦?
只能添加唄,如下圖所示,完蛋了........
代碼演化小結
看得出來,《最新版俠客行》
解決了復雜判斷的問題,也解決了訪問者擴展的問題
但是
對於被訪問者的類型的擴展,顯然是沒有擴展性的,不符合開閉原則
這一點體現出來了這種解決方法的
傾向性,傾向於
擴展行為,可以自如的增加新的行為
但是
不能輕松的增加元素類型
測試代碼Test類不需要修改
看一下打印結果
最新版俠客行結構
回首意圖
再回頭看下訪問者模式的意圖
表示一個作用於某對象結構中的各元素的操作。它使你可以在不改變各元素類的前提下定義作用於這些元素的新操作。
就是上面示例中,對於來訪者的擴展嘛
最初的動機就是處理《俠客行》中類似的問題
集合容器中保存了不同類型的對象,他們又可能有多種不同場景的操作
比如一份名單,班長可能拿過去收作業,班主任拿過去可能點名
名單里面都有你也有他,你就是那個你,他還是那個他,但是你的作業是你的作業,他的作業是他的作業。
所以對於班長和班主任兩個訪問者,同學們的行為是不一樣的,對同一來訪者,不同的同學的行為又是不一樣的
結構
抽象元素角色Element
抽象元素一般是抽象類或者接口
通常它定義一個 accept(抽象訪問者) 方法,用來將自身傳遞給訪問者
具體的元素角色ConcreateElement
具體元素實現了accept方法,在accept方法中調用訪問者的訪問方法以便完成對一個元素的操作
抽象訪問者Visitor
定義一個或者多個訪問操作
抽象訪問者需要面向具體的被訪問者元素類型,所以有幾個具體的元素類型需要被訪問,就有幾個重載方法
具體的訪問者ConcreateVisitor
具體的訪問者封裝了不同訪問者,不同類型對象的具體行為,也就是最終的分情況的處理邏輯
對象結構ObjectStructure
對象結構是元素的集合,用於存放元素的對象,並且一般提供遍歷內部元素的方法
客戶端角色Client
組織被訪問者,然后通過訪問者訪問
訪問者模式有兩個主要層次,訪問者以及被訪問元素
訪問者有不同的類型,被訪問元素有不同的類型
每一種訪問者對於每一種被訪問元素都有一種不同的行為,這不同的行為是封裝在訪問者的方法中
所以訪問者需要進行訪問方法visit的重載,
被訪問元素有幾種類型,就有幾種重載版本
面向細節的邏輯既然被封裝在訪問者中,被訪問元素就不需要面向細節了,只需要把自己的類型傳遞給訪問者即可
所以,
所有的被訪問元素都只有一個版本的accept方法
概念示例代碼
我們可以抽象化的看下下面的例子
下面的代碼很簡單,A有三種子類型,B有三種子類型
不同的A和不同的B,將會擦出不一樣的火花,也就是會出現9種可能的場景
將A定義為訪問者,那么A就要借助方法的重載實現不同類型被訪問者B的不同行為
而將方法的調用轉變為被訪問者的反向調用----this傳遞給訪問者
package visitor; public class example { public static void main(String[] args) { A1 a1 = new A1(); A2 a2 = new A2(); A3 a3 = new A3(); B1 b1 = new B1(); B2 b2 = new B2(); B3 b3 = new B3(); b1.accept(a1); b1.accept(a2); b1.accept(a3); b2.accept(a1); b2.accept(a2); b2.accept(a3); b3.accept(a1); b3.accept(a2); b3.accept(a3); } } abstract class A { abstract void visit(B1 b1); abstract void visit(B2 b2); abstract void visit(B3 b3); } class A1 extends A { @Override void visit(B1 b1) { System.out.println("A1 play with B1"); } @Override void visit(B2 b2) { System.out.println("A1 play with B2"); } @Override void visit(B3 b3) { System.out.println("A1 play with B3"); } } class A2 extends A { @Override void visit(B1 b1) { System.out.println("A2 play with B1"); } @Override void visit(B2 b2) { System.out.println("A2 play with B2"); } @Override void visit(B3 b3) { System.out.println("A2 play with B3"); } } class A3 extends A { @Override void visit(B1 b1) { System.out.println("A3 play with B1"); } @Override void visit(B2 b2) { System.out.println("A3 play with B2"); } @Override void visit(B3 b3) { System.out.println("A3 play with B3"); } } abstract class B { abstract void accept(A a); } class B1 extends B { @Override void accept(A a) { a.visit(this); } } class B2 extends B { @Override void accept(A a) { a.visit(this); } } class B3 extends B { @Override void accept(A a) { a.visit(this); } }
這種重載和回傳自身的形式,完全可以當作一個套路來使用,對於這種組合形式的場景,非常受用。
訪問者的自身借助多態特性,又依賴方法重載,然后再借助於this回傳達到反向確定類型調用,真心精巧。
總結
訪問者模式靈活的處理了不同類型的元素,面對不同的訪問者,有不同的行為的場景。
這種組合場景,判斷邏輯復雜繁瑣,訪問者模式可以做到靈活的擴展增加更多的行為,而不需要改變原來的類。
訪問者模式傾向於擴展元素的行為,當擴展元素行為時,滿足開閉原則
但是對於擴展新的元素類型時,將會產生巨大的改動,每一個訪問者都需要變動,所以在使用訪問者模式是要考慮清楚元素類型的變化可能。
因為訪問者依賴的是具體的元素,而不是抽象元素,所以才難以擴展
訪問者依賴的是具體元素,而不是抽象元素,這破壞了依賴倒置原則,特別是在面向對
象的編程中,拋棄了對接口的依賴,而直接依賴實現類,擴展比較難。
當業務規則需要遍歷多個不同的對象時,而且不同的對象在不同的場景下又有不同的行為
你就應該考慮使用訪問者模式
如果對象結構中的對象不常變化,但是他們的行為卻經常變化時,也可以考慮使用,訪問者模式可以很靈活的擴展新的訪客。