1.初識(shí)訪問者模式
表示一個(gè)作用于某對(duì)象結(jié)構(gòu)中的各元素的操作带污。它使你可以在不改變各元素的類的前提下定義作用于這些元素的新操作趁俊。
- Visitor:訪問者接口,為所有的訪問者對(duì)象聲明一個(gè)visit方法,用來代表為對(duì)象結(jié)構(gòu)添加的功能话原,理論上可以代表任意的功能。
ConcreteVisitor:具體訪問者實(shí)現(xiàn)對(duì)象芯咧,實(shí)現(xiàn)要真正被添加到對(duì)象結(jié)構(gòu)中的功能奏赘。
Element:抽象的元素對(duì)象,對(duì)象結(jié)構(gòu)的頂層接口幕屹,定義接受訪問的操作蓝丙。
ConcreteElement:具體元素對(duì)象,對(duì)象結(jié)構(gòu)中具體的對(duì)象望拖,也是被訪問的對(duì)象渺尘,通常會(huì)回調(diào)訪問者的真實(shí)功能,同時(shí)開放自身的數(shù)據(jù)供訪問者使用说敏。
ObjectStructure:對(duì)象結(jié)構(gòu)鸥跟,通常包含多個(gè)被訪問的對(duì)象,它可以遍歷這多個(gè)被訪問的對(duì)象盔沫,也可以讓訪問者訪問它的元素医咨。可以是一個(gè)復(fù)合或是一個(gè)集合迅诬,如一個(gè)列表或無序集合腋逆。
但是請(qǐng)注意:這個(gè)ObjectStructure并不是我們?cè)谇懊嬷v到的對(duì)象結(jié)構(gòu),前面一直講的對(duì)象結(jié)構(gòu)是指的一系列對(duì)象的定義結(jié)構(gòu)侈贷,是概念上的東西惩歉;而 ObjectStructure可以看成是對(duì)象結(jié)構(gòu)中的一系列對(duì)象的一個(gè)集合,是用來輔助客戶端訪問這一系列對(duì)象的俏蛮,所以為了不造成大家的困惑撑蚌,后面提到 ObjectStructure的時(shí)候,就用英文名稱來代替搏屑,不把它翻譯成中文争涌。
2.體會(huì)訪問者模式
2.1 場(chǎng)景問題——擴(kuò)展客戶管理的功
能考慮這樣一個(gè)應(yīng)用:擴(kuò)展客戶管理的功能。
既然是擴(kuò)展功能辣恋,那么肯定是已經(jīng)存在一定的功能了亮垫,先看看已有的功
能:公司的客戶分成兩大類模软,一類是企業(yè)客戶,一類是個(gè)人客戶饮潦,現(xiàn)有的功能非常簡(jiǎn)單燃异,就是能讓客戶提出服務(wù)申請(qǐng)。目前的程序結(jié)構(gòu)如圖
隨著業(yè)務(wù)的發(fā)展继蜡,需要加強(qiáng)客戶管理的功能回俐,假設(shè)現(xiàn)需增加如下的功能:
- 1)客戶對(duì)公司產(chǎn)品的偏好分析,針對(duì)企業(yè)客戶和個(gè)人客戶有不同的分析策略稀并,主要是根據(jù)以往購買的歷史仅颇、潛在購買意向等進(jìn)行分析,對(duì)于企業(yè)客戶還要添加上客戶所在行業(yè)的發(fā)展趨勢(shì)碘举、客戶的發(fā)展預(yù)期等的分析忘瓦。
- 2)客戶價(jià)值分析,針對(duì)企業(yè)客戶和個(gè)人客戶引颈,有不同的分析方式和策略政冻。主要是根據(jù)購買的金額大小、購買的產(chǎn)品和服務(wù)的多少线欲、購買的頻率等進(jìn)行分析。
其實(shí)除了這些功能汽摹,還有很多潛在的功能李丰,只是現(xiàn)在還沒有要求實(shí)現(xiàn),比如:針對(duì)不同的客戶進(jìn)行需求調(diào)查逼泣;針對(duì)不同的客戶進(jìn)行滿意度分析趴泌;客戶消費(fèi)預(yù)期分析等等。雖然現(xiàn)在沒有要求實(shí)現(xiàn)拉庶,但不排除今后有可能會(huì)要求實(shí)現(xiàn)嗜憔。
2.2 不用模式的解決方案
2.2.1 實(shí)現(xiàn)思路
要實(shí)現(xiàn)上面要求的功能,也不是很困難氏仗,一個(gè)很基本的想法就是:既然不同類型的客戶操作是不同的吉捶,那么在不同類型的客戶里面分別實(shí)現(xiàn)這些功能,不就可以了皆尔。
由于這些功能的實(shí)現(xiàn)依附于很多其它功能的實(shí)現(xiàn)呐舔,或者是需要很多其它的業(yè)務(wù)數(shù)據(jù),在示例里面不太好完整的體現(xiàn)其功能實(shí)現(xiàn)慷蠕,都是示意一下珊拼,因此提前說明一下。
按照上述的想法流炕,這個(gè)時(shí)候的程序結(jié)構(gòu)如圖
2.2.2 有何問題
以很簡(jiǎn)單的方式澎现,實(shí)現(xiàn)了要求的功能仅胞,這種實(shí)現(xiàn)有沒有什么問題呢?仔細(xì)
分析上面的實(shí)現(xiàn)剑辫,發(fā)現(xiàn)有兩個(gè)主要的問題:
- 1)在企業(yè)客戶和個(gè)人客戶的類里面干旧,都分別實(shí)現(xiàn)了提出服務(wù)請(qǐng)求、進(jìn)行產(chǎn)品偏好分析揭斧、進(jìn)行客戶價(jià)值分析等功能莱革,也就是說,這些功能的實(shí)現(xiàn)代碼是混雜在同一個(gè)類里面的讹开;而且相同的功能分散到了不同的類中去實(shí)現(xiàn)盅视,這會(huì)導(dǎo)致整個(gè)系統(tǒng)難以理解、難以維護(hù)旦万。
- 2)更為痛苦的是闹击,采用這樣的實(shí)現(xiàn)方式,如果要給客戶擴(kuò)展新的功能成艘,比如前面提到的針對(duì)不同的客戶進(jìn)行需求調(diào)查赏半;針對(duì)不同的客戶進(jìn)行滿意度分析;客戶消費(fèi)預(yù)期分析等等淆两。每次擴(kuò)展断箫,都需要改動(dòng)企業(yè)客戶的類和個(gè)人客戶的類,當(dāng)然也可以通過為它們擴(kuò)展子類的方式秋冰,但是這樣可能會(huì)造成過多的對(duì)象層次仲义。
那么有沒有辦法,能夠在不改變客戶這個(gè)對(duì)象結(jié)構(gòu)中各元素類的前提下剑勾,為這些類定義新的功能埃撵?也就是要求不改變企業(yè)客戶和個(gè)人客戶類,就能為企業(yè)客戶和個(gè)人客戶類定義新的功能虽另?
2.3 使用模式的解決方案
仔細(xì)分析上面的示例暂刘,對(duì)于客戶這個(gè)對(duì)象結(jié)構(gòu),不想改變類捂刺,又要添加新的功能谣拣,很明顯就需要一種動(dòng)態(tài)的方式,在運(yùn)行期間把功能動(dòng)態(tài)地添加到對(duì)象結(jié)構(gòu)中去族展。
有些朋友可能會(huì)想起裝飾模式芝发,裝飾模式可以實(shí)現(xiàn)為一個(gè)對(duì)象透明的添加功能,但裝飾模式基本上是在現(xiàn)有的功能的基礎(chǔ)之上進(jìn)行功能添加苛谷,實(shí)際上是對(duì)現(xiàn)有功能的加強(qiáng)或者改造辅鲸。并不是在現(xiàn)有功能不改動(dòng)的情況下,為對(duì)象添加新的功能腹殿。
看來需要另外尋找新的解決方式了独悴,可以應(yīng)用訪問者模式來解決這個(gè)問題例书。要使用訪問者模式來重寫示例,首先就要按照訪問者模式的結(jié)構(gòu)刻炒,分離出兩個(gè)類層次來决采,一個(gè)是對(duì)應(yīng)于元素的類層次,一個(gè)是對(duì)應(yīng)于訪問者的類層次坟奥。
對(duì)于對(duì)應(yīng)于元素的類層次树瞭,現(xiàn)在已經(jīng)有了,就是客戶的對(duì)象層次爱谁。而對(duì)應(yīng)于訪問者的類層次晒喷,現(xiàn)在還沒有,不過访敌,按照訪問者模式的結(jié)構(gòu)凉敲,應(yīng)該是先定義一個(gè)訪問者接口,然后把每種業(yè)務(wù)實(shí)現(xiàn)成為一個(gè)單獨(dú)的訪問者對(duì)象寺旺,也就是說應(yīng)該使用一個(gè)訪問者對(duì)象來實(shí)現(xiàn)對(duì)客戶的偏好分析爷抓,而用另外一個(gè)訪問者對(duì)象來實(shí)現(xiàn)對(duì)客戶的價(jià)值分析。
在分離好兩個(gè)類層次過后阻塑,為了方便客戶端的訪問蓝撇,定義一個(gè)ObjectStructure,其實(shí)就類似于前面示例中的客戶管理的業(yè)務(wù)對(duì)象陈莽。新的示例的結(jié)構(gòu)如圖:
3.理解訪問者模式
3.1 認(rèn)識(shí)訪問者模式
3.1.1 訪問者的功能
訪問者模式能給一系列對(duì)象唉地,透明的添加新功能。從而避免在維護(hù)期間传透,對(duì)這一系列對(duì)象進(jìn)行修改,而且還能變相實(shí)現(xiàn)復(fù)用訪問者所具有的功能极颓。
由于是針對(duì)一系列對(duì)象的操作朱盐,這也導(dǎo)致,如果只想給一系列對(duì)象中的部分對(duì)象添加功能菠隆,就會(huì)有些麻煩兵琳;而且要始終能保證把這一系列對(duì)象都要調(diào)用到,不管是循環(huán)也好骇径,還是遞歸也好躯肌,總之要讓每個(gè)對(duì)象都要被訪問到。
3.1.2 調(diào)用通路
訪問者之所以能實(shí)現(xiàn)“為一系列對(duì)象透明的添加新功能 ”破衔,注意是透明的清女,也就是這一系列對(duì)象是不知道被添加功能的。
重要的就是依靠通用方法晰筛,訪問者這邊說要去訪問嫡丙,就提供一個(gè)訪問的方法拴袭,如 visit方法;而對(duì)象那邊說曙博,好的拥刻,我接受你的訪問,提供一個(gè)接受訪問的方法父泳,如accept方法般哼。這兩個(gè)方法并不代表任何具體的功能,只是構(gòu)成一個(gè)調(diào)用的通路惠窄,那么真正的功能實(shí)現(xiàn)在哪里呢蒸眠?又如何調(diào)用到呢?
很簡(jiǎn)單睬捶,就在 accept方法里面黔宛,回調(diào)visit的方法,從而回調(diào)到訪問者的具體實(shí)現(xiàn)上擒贸,而這個(gè)訪問者的具體實(shí)現(xiàn)的方法才是要添加的新的功能臀晃。
3.1.3 兩次分發(fā)技術(shù)
訪問者模式能夠?qū)崿F(xiàn)在不改變對(duì)象結(jié)構(gòu)的情況下,就能給對(duì)象結(jié)構(gòu)中的類增加功能介劫,實(shí)現(xiàn)這個(gè)效果所使用的核心技術(shù)就是兩次分發(fā)的技術(shù)徽惋。
在訪問者模式中,當(dāng)客戶端調(diào)用ObjectStructure的時(shí)候座韵,會(huì)遍歷 ObjectStructure中所有的元素险绘,調(diào)用這些元素的accept方法,讓這些元素來接受訪問誉碴,這是請(qǐng)求的第一次分發(fā)宦棺;在具體的元素對(duì)象中實(shí)現(xiàn)accept方法的時(shí)候,會(huì)回調(diào)訪問者的visit方法黔帕,等于請(qǐng)求被第二次分發(fā)了代咸,請(qǐng)求被分發(fā)給訪問者來進(jìn)行處理,真正實(shí)現(xiàn)功能的正是訪問者的visit方法成黄。
兩次分發(fā)技術(shù)具體的調(diào)用過程示意如圖:
兩次分發(fā)技術(shù)使得客戶端的請(qǐng)求不再被靜態(tài)的綁定在元素對(duì)象上呐芥,這個(gè)時(shí)候真正執(zhí)行什么樣的功能同時(shí)取決于訪問者類型和元素類型,就算是同一種元素類型奋岁,只要訪問者類型不一樣,最終執(zhí)行的功能也不會(huì)一樣闻伶,這樣一來,就可以在元素對(duì)象不變的情況下更鲁,通過改變?cè)L問者的類型澡为,來改變真正執(zhí)行的功能景埃。
兩次分發(fā)技術(shù)還有一個(gè)優(yōu)點(diǎn)媒至,就是可以在程序運(yùn)行期間進(jìn)行動(dòng)態(tài)的功能組裝和切換,只需要在客戶端調(diào)用時(shí)谷徙,組合使用不同的訪問者對(duì)象實(shí)例即可拒啰。
從另一個(gè)層面思考,Java回調(diào)技術(shù)也有點(diǎn)類似于兩次分發(fā)技術(shù)完慧,客戶端調(diào)用某方法谋旦,這個(gè)方法就類似于accept方法,傳入一個(gè)接口的實(shí)現(xiàn)對(duì)象屈尼,這個(gè)接口的實(shí)現(xiàn)對(duì)象就有點(diǎn)像是訪問者册着,在方法內(nèi)部,會(huì)回調(diào)這個(gè)接口的方法脾歧,就類似于調(diào)用訪問者的visit方法甲捏,最終執(zhí)行的還是接口的具體實(shí)現(xiàn)里面實(shí)現(xiàn)的功能。
3.1.4 為何不在Component中實(shí)現(xiàn)回調(diào)visit方法
在看上面的示例的時(shí)候鞭执,細(xì)心的朋友會(huì)發(fā)現(xiàn)司顿,在企業(yè)客戶對(duì)象和個(gè)人客戶
對(duì)象中實(shí)現(xiàn)的accept方法從表面上看是相似的,都需要回調(diào)訪問者的方法兄纺,可能就會(huì)有朋友想大溜,為什么不把回調(diào)訪問者方法的調(diào)用語句放到父類中去,那樣不就可以復(fù)用了嗎?
請(qǐng)注意,這是不可以的限佩,雖然看起來是相似的語句,但其實(shí)是不同的泞坦,主
要的玄機(jī)就在傳入的this身上。 this是代表當(dāng)前的對(duì)象實(shí)例的,在企業(yè)客戶對(duì)象中傳遞的就是企業(yè)客戶對(duì)象的實(shí)例锣险,在個(gè)人客戶對(duì)象中傳遞的就是個(gè)人客戶對(duì)象的實(shí)例,這樣在訪問者的實(shí)現(xiàn)中,就可以通過這不同的對(duì)象實(shí)例來訪問不同的實(shí)例對(duì)象的數(shù)據(jù)了掩幢。
如果把這句話放到父類中,那么傳遞的就是父類對(duì)象的實(shí)例,是沒有子對(duì)象的數(shù)據(jù)的轮听,因此這句話不能放到父類中去。
3.1.5 訪問者模式的調(diào)用順序示意圖
3.1.6 空的訪問方法
并不是所有的訪問方法都需要實(shí)現(xiàn)柿隙,由于訪問者模式默認(rèn)的是訪問對(duì)象結(jié)構(gòu)中的所有元素,因此在實(shí)現(xiàn)某些功能的時(shí)候,如果不需要涉及到某些元素的訪問方法叶雹,這些方法可以實(shí)現(xiàn)成為空的,比如:這個(gè)訪問者只想要處理組合對(duì)象,那么訪問葉子對(duì)象的方法就可以為空缕探,雖然還是需要訪問所有的元素對(duì)象。
還有一種就是有條件接受訪問倦始,在自己的accept方法里面進(jìn)行判斷,滿足要求的接受,不滿足要求的肮雨,就相當(dāng)于空的訪問方法,什么都不用做椅亚。
3.2 操作組合對(duì)象結(jié)構(gòu)
對(duì)于使用組合模式構(gòu)建的組合對(duì)象結(jié)構(gòu),對(duì)外有一個(gè)統(tǒng)一的外觀,要想添加新的功能也不是很困難惧磺,只要在組件的接口上定義新的功能就可以了,麻煩的是這樣一來番捂,需要修改所有的子類。而且鳖枕,每次添加一個(gè)新功能,都需要這么痛苦一回,修改組件接口则奥,然后修改所有的子類,這是相當(dāng)糟糕的罚舱。
為了讓組合對(duì)象結(jié)構(gòu)更靈活粥脚、更容易維護(hù)和更好的擴(kuò)展性,接下來把它改造成訪問者模式和組合模式組合來實(shí)現(xiàn)树灶。這樣在今后再進(jìn)行功能改造的時(shí)候熄驼,就不需要再改動(dòng)這個(gè)組合對(duì)象結(jié)構(gòu)了萝映。
訪問者模式和組合模式組合使用的思路:首先把組合對(duì)象結(jié)構(gòu)中的功能方法分離出來实束,雖然維護(hù)組合對(duì)象結(jié)構(gòu)的方法也可以分離出來构订,但是為了維持組合對(duì)象結(jié)構(gòu)本身,這些方法還是放在組合對(duì)象結(jié)構(gòu)里面亥宿;然后把這些功能方法分別實(shí)現(xiàn)成為訪問者對(duì)象,通過訪問者模式添加到組合的對(duì)象結(jié)構(gòu)中去悟狱。
下面通過訪問者模式和組合模式組合來實(shí)現(xiàn)如下功能:輸出對(duì)象的名稱,在組合對(duì)象的名稱前面添加 “節(jié)點(diǎn):”富稻,在葉子對(duì)象的名稱前面添加“葉子:”抚岗。
小結(jié)現(xiàn)在的程序結(jié)構(gòu)
前面是分步的示范认境,大家已經(jīng)體會(huì)了一番,接下來小結(jié)一下硅急。
如同前面的示例丑罪,訪問者的方法就相當(dāng)于作用于組合對(duì)象結(jié)構(gòu)中各個(gè)元素的操作啸驯,是一種通用的表達(dá),同樣的訪問者接口和同樣的方法袱吆,只要提供不同的訪問者具體實(shí)現(xiàn)蓬衡,就表示不同的功能。
同時(shí)在組合對(duì)象中,接受訪問的方法,也是一個(gè)通用的表達(dá)携取,不管你是什么樣的功能惊豺,統(tǒng)統(tǒng)接受就好了,然后回調(diào)回去執(zhí)行真正的功能烹俗。這樣一來,各元素的類就不用再修改了萍程,只要提供不同的訪問者實(shí)現(xiàn)擅笔,然后通過這個(gè)通用表達(dá)伤塌,就結(jié)合到組合對(duì)象中來了瑰妄,就相當(dāng)于給所有的對(duì)象提供了新的功能翅楼。
示例的整體結(jié)構(gòu):
3.3 誰負(fù)責(zé)遍歷所有元素對(duì)象
在訪問者模式中,訪問者必須要能夠訪問到對(duì)象結(jié)構(gòu)中的每個(gè)對(duì)象,因?yàn)樵L問者要為每個(gè)對(duì)象添加功能潮尝,為此特別在模式中定義出一個(gè)ObjectStructure來羹蚣,然后由ObjectStructure來負(fù)責(zé)遍歷訪問一系列對(duì)象中的每個(gè)對(duì)象胁出。
1.在ObjectStructure迭代所有的元素時(shí)型凳,又分成兩種情況槐脏。
- 1)一種是元素的對(duì)象結(jié)構(gòu)是通過集合來組織的,那么直接在ObjectStructure中對(duì)集合進(jìn)行迭代畴蒲,對(duì)每一個(gè)元素調(diào)用accept就好了。如同前面示例所采用的方式溶其。
- 2)另一種情況是元素的對(duì)象結(jié)構(gòu)是通過組合模式來組織的浓利,通常可以構(gòu)成對(duì)象樹,這種情況一般就不需要在ObjectStructure中迭代了,而通常的做法是在組合對(duì)象的accept方法里面秸应,遞歸遍歷它的子元素,然后調(diào)用子元素的accept方法凡蚜,如同前面示例中Composite的實(shí)現(xiàn),在accept方法里面進(jìn)行遞歸調(diào)用子對(duì)象的操作。
2.不需要ObjectStructure的時(shí)候
在實(shí)際開發(fā)中,有一種典型的情況可以不需要ObjectStructure對(duì)象耐薯,那就是只有一個(gè)被訪問對(duì)象的時(shí)候靠柑。只有一個(gè)被訪問對(duì)象扒磁,當(dāng)然就不需要使用 ObjectStructure來組合和迭代了敦腔,只要調(diào)用這個(gè)對(duì)象就好了锄蹂。
事實(shí)上還有一種情況也可以不使用ObjectStructure砌滞,比如上面訪問的組合對(duì)象結(jié)構(gòu)侮邀,從客戶端的角度看,他訪問的其實(shí)就是一個(gè)對(duì)象贝润,因此可以把 ObjectStructure去掉绊茧,然后直接從客戶端調(diào)用元素的accept方法。
3.有些時(shí)候打掘,遍歷元素的方法也可以放到訪問者當(dāng)中去华畏,當(dāng)然也是需要遞歸遍歷它的子元素的鹏秋。出現(xiàn)這種情況的主要原因是:想在訪問者中實(shí)現(xiàn)特別復(fù)雜的遍歷,訪問者的實(shí)現(xiàn)依賴于對(duì)象結(jié)構(gòu)的操作結(jié)果亡笑。
前面的示例已經(jīng)實(shí)現(xiàn)了:使用訪問者模式和組合模式組合來實(shí)現(xiàn)了輸出名稱的功能拼岳,如果現(xiàn)在要實(shí)現(xiàn)把組合的對(duì)象結(jié)構(gòu)按照樹的形式輸出,就是按照在組合模式中示例的那樣况芒,輸出如下的樹形結(jié)構(gòu):
要實(shí)現(xiàn)這個(gè)功能惜纸,在組合對(duì)象結(jié)構(gòu)中去遍歷子對(duì)象的方式就比較難于實(shí)現(xiàn),因?yàn)橐敵鲞@個(gè)樹形結(jié)構(gòu)绝骚,需要控制每個(gè)對(duì)象在輸出的時(shí)候耐版,向后的退格數(shù)量,這個(gè)需要在對(duì)象結(jié)構(gòu)的循環(huán)中來控制压汪,這種功能可以選擇在訪問者當(dāng)中去遍歷對(duì)象結(jié)構(gòu)粪牲。
3.4 訪問者模式的優(yōu)缺點(diǎn)
- 好的擴(kuò)展性
- 好的復(fù)用性
- 分離無關(guān)行為
- 對(duì)象結(jié)構(gòu)變化很困難
- 破壞封裝
4.思考訪問者模式
4.1 訪問者模式的本質(zhì)
預(yù)留通路,回調(diào)實(shí)現(xiàn)
4.2 何時(shí)選用
- 1)如果想對(duì)一個(gè)對(duì)象結(jié)構(gòu)止剖,實(shí)施一些依賴于對(duì)象結(jié)構(gòu)中的具體類的操作腺阳,可以使用訪問者模式
- 2)如果想對(duì)一個(gè)對(duì)象結(jié)構(gòu)中的各個(gè)元素,進(jìn)行很多不同的而且不相關(guān)的操作穿香,為了避免這些操作使得類變得雜亂亭引,可以使用訪問者模式,把這些操作分散到不同的訪問者對(duì)象中去皮获,每個(gè)訪問者對(duì)象實(shí)現(xiàn)同一類功能焙蚓。
- 3)如果對(duì)象結(jié)構(gòu)很少變動(dòng),但是需要經(jīng)常給對(duì)象結(jié)構(gòu)中的元素對(duì)象定義新的操作洒宝,可以使用訪問者模式