【學(xué)習(xí)難度:★★☆☆☆悔橄,使用頻率:★★★☆☆】
直接出處:訪問者模式
梳理和學(xué)習(xí):https://github.com/BruceOuyang/boy-design-pattern
簡書日期: 2018/04/03
簡書首頁:http://www.reibang.com/p/0fb891a7c5ed
操作復(fù)雜對(duì)象結(jié)構(gòu)——訪問者模式(一)
想必大家都去過醫(yī)院瘤袖,雖然沒有人喜歡去醫(yī)院(愛崗敬業(yè)的醫(yī)務(wù)工作人員除外,微笑)冷溶。在醫(yī)生開具處方單(藥單)后,很多醫(yī)院都存在如下處理流程:劃價(jià)人員拿到處方單之后根據(jù)藥品名稱和數(shù)量計(jì)算總價(jià),藥房工作人員根據(jù)藥品名稱和數(shù)量準(zhǔn)備藥品岩喷,如圖26-1所示:
在圖26-1中售碳,我們可以將處方單看成一個(gè)藥品信息的集合强重,里面包含了一種或多種不同類型的藥品信息,不同類型的工作人員(如劃價(jià)人員和藥房工作人員)在操作同一個(gè)藥品信息集合時(shí)將提供不同的處理方式贸人,而且可能還會(huì)增加新類型的工作人員來操作處方單间景。
在軟件開發(fā)中,有時(shí)候我們也需要處理像處方單這樣的集合對(duì)象結(jié)構(gòu)艺智,在該對(duì)象結(jié)構(gòu)中存儲(chǔ)了多個(gè)不同類型的對(duì)象信息倘要,而且對(duì)同一對(duì)象結(jié)構(gòu)中的元素的操作方式并不唯一,可能需要提供多種不同的處理方式,還有可能增加新的處理方式封拧。在設(shè)計(jì)模式中志鹃,有一種模式可以滿足上述要求,其模式動(dòng)機(jī)就是以不同的方式操作復(fù)雜對(duì)象結(jié)構(gòu)泽西,該模式就是我們本章將要介紹的訪問者模式曹铃。
26.1 OA系統(tǒng)中員工數(shù)據(jù)匯總
Sunny軟件公司欲為某銀行開發(fā)一套OA系統(tǒng),在該OA系統(tǒng)中包含一個(gè)員工信息管理子系統(tǒng)捧杉,該銀行員工包括正式員工和臨時(shí)工陕见,每周人力資源部和財(cái)務(wù)部等部門需要對(duì)員工數(shù)據(jù)進(jìn)行匯總,匯總數(shù)據(jù)包括員工工作時(shí)間味抖、員工工資等评甜。該公司基本制度如下:
(1) 正式員工(Full time Employee)每周工作時(shí)間為40小時(shí),不同級(jí)別仔涩、不同部門的員工每周基本工資不同忍坷;如果超過40小時(shí),超出部分按照100元/小時(shí)作為加班費(fèi)红柱;如果少于40小時(shí)承匣,所缺時(shí)間按照請(qǐng)假處理,請(qǐng)假所扣工資以80元/小時(shí)計(jì)算锤悄,直到基本工資扣除到零為止韧骗。除了記錄實(shí)際工作時(shí)間外,人力資源部需記錄加班時(shí)長或請(qǐng)假時(shí)長零聚,作為員工平時(shí)表現(xiàn)的一項(xiàng)依據(jù)袍暴。
(2) 臨時(shí)工(Part time Employee)每周工作時(shí)間不固定,基本工資按小時(shí)計(jì)算隶症,不同崗位的臨時(shí)工小時(shí)工資不同政模。人力資源部只需記錄實(shí)際工作時(shí)間。
人力資源部和財(cái)務(wù)部工作人員可以根據(jù)各自的需要對(duì)員工數(shù)據(jù)進(jìn)行匯總處理蚂会,人力資源部負(fù)責(zé)匯總每周員工工作時(shí)間淋样,而財(cái)務(wù)部負(fù)責(zé)計(jì)算每周員工工資。
Sunny軟件公司開發(fā)人員針對(duì)上述需求胁住,提出了一個(gè)初始解決方案趁猴,其核心代碼如下所示:
import java.util.*;
class EmployeeList
{
private ArrayList<Employee> list = new ArrayList<Employee>(); //員工集合
//增加員工
public void addEmployee(Employee employee)
{
list.add(employee);
}
//處理員工數(shù)據(jù)
public void handle(String departmentName)
{
if(departmentName.equalsIgnoreCase("財(cái)務(wù)部")) //財(cái)務(wù)部處理員工數(shù)據(jù)
{
for(Object obj : list)
{
if(obj.getClass().getName().equalsIgnoreCase("FulltimeEmployee"))
{
System.out.println("財(cái)務(wù)部處理全職員工數(shù)據(jù)!");
}
else
{
System.out.println("財(cái)務(wù)部處理兼職員工數(shù)據(jù)彪见!");
}
}
}
else if(departmentName.equalsIgnoreCase("人力資源部")) //人力資源部處理員工數(shù)據(jù)
{
for(Object obj : list)
{
if(obj.getClass().getName().equalsIgnoreCase("FulltimeEmployee"))
{
System.out.println("人力資源部處理全職員工數(shù)據(jù)儡司!");
}
else
{
System.out.println("人力資源部處理兼職員工數(shù)據(jù)!");
}
}
}
}
}
在EmployeeList類的handle()方法中余指,通過對(duì)部門名稱和員工類型進(jìn)行判斷捕犬,不同部門對(duì)不同類型的員工進(jìn)行了不同的處理,滿足了員工數(shù)據(jù)匯總的要求。但是該解決方案存在如下幾個(gè)問題:
(1) EmployeeList類非常龐大碉碉,它將各個(gè)部門處理各類員工數(shù)據(jù)的代碼集中在一個(gè)類中柴钻,在具體實(shí)現(xiàn)時(shí),代碼將相當(dāng)冗長垢粮,EmployeeList類承擔(dān)了過多的職責(zé)顿颅,既不方便代碼的復(fù)用,也不利于系統(tǒng)的擴(kuò)展足丢,違背了“單一職責(zé)原則”。
(2)在代碼中包含大量的“if…else…”條件判斷語句庇配,既需要對(duì)不同部門進(jìn)行判斷斩跌,又需要對(duì)不同類型的員工進(jìn)行判斷,還將出現(xiàn)嵌套的條件判斷語句捞慌,導(dǎo)致測(cè)試和維護(hù)難度增大耀鸦。
(3)如果要增加一個(gè)新的部門來操作員工集合,不得不修改EmployeeList類的源代碼啸澡,在handle()方法中增加一個(gè)新的條件判斷語句和一些業(yè)務(wù)處理代碼來實(shí)現(xiàn)新部門的訪問操作袖订。這違背了“開閉原則”,系統(tǒng)的靈活性和可擴(kuò)展性有待提高嗅虏。
(4)如果要增加一種新類型的員工洛姑,同樣需要修改EmployeeList類的源代碼,在不同部門的處理代碼中增加對(duì)新類型員工的處理邏輯皮服,這也違背了“開閉原則”楞艾。 如何解決上述問題?如何為同一集合對(duì)象中的元素提供多種不同的操作方式龄广?訪問者模式就是一個(gè)值得考慮的解決方案硫眯,它可以在一定程度上解決上述問題(解決大部分問題)。訪問者模式可以為為不同類型的元素提供多種訪問操作方式择同,而且可以在不修改原有系統(tǒng)的情況下增加新的操作方式两入。
操作復(fù)雜對(duì)象結(jié)構(gòu)——訪問者模式(二)
26.2 訪問者模式概述
訪問者模式是一種較為復(fù)雜的行為型設(shè)計(jì)模式,它包含訪問者和被訪問元素兩個(gè)主要組成部分敲才,這些被訪問的元素通常具有不同的類型裹纳,且不同的訪問者可以對(duì)它們進(jìn)行不同的訪問操作。例如處方單中的各種藥品信息就是被訪問的元素归斤,而劃價(jià)人員和藥房工作人員就是訪問者痊夭。訪問者模式使得用戶可以在不修改現(xiàn)有系統(tǒng)的情況下擴(kuò)展系統(tǒng)的功能,為這些不同類型的元素增加新的操作脏里。
在使用訪問者模式時(shí)她我,被訪問元素通常不是單獨(dú)存在的,它們存儲(chǔ)在一個(gè)集合中,這個(gè)集合被稱為“對(duì)象結(jié)構(gòu)”番舆,訪問者通過遍歷對(duì)象結(jié)構(gòu)實(shí)現(xiàn)對(duì)其中存儲(chǔ)的元素的逐個(gè)操作酝碳。
訪問者模式定義如下:
訪問者模式(Visitor Pattern):提供一個(gè)作用于某對(duì)象結(jié)構(gòu)中的各元素的操作表示,它使我們可以在不改變各元素的類的前提下定義作用于這些元素的新操作恨狈。訪問者模式是一種對(duì)象行為型模式疏哗。
訪問者模式的結(jié)構(gòu)較為復(fù)雜,其結(jié)構(gòu)如圖26-2所示:
在訪問者模式結(jié)構(gòu)圖中包含如下幾個(gè)角色:
Vistor(抽象訪問者):抽象訪問者為對(duì)象結(jié)構(gòu)中每一個(gè)具體元素類ConcreteElement聲明一個(gè)訪問操作禾怠,從這個(gè)操作的名稱或參數(shù)類型可以清楚知道需要訪問的具體元素的類型返奉,具體訪問者需要實(shí)現(xiàn)這些操作方法,定義對(duì)這些元素的訪問操作吗氏。
ConcreteVisitor(具體訪問者):具體訪問者實(shí)現(xiàn)了每個(gè)由抽象訪問者聲明的操作芽偏,每一個(gè)操作用于訪問對(duì)象結(jié)構(gòu)中一種類型的元素。
Element(抽象元素):抽象元素一般是抽象類或者接口弦讽,它定義一個(gè)accept()方法污尉,該方法通常以一個(gè)抽象訪問者作為參數(shù)⊥【稍后將介紹為什么要這樣設(shè)計(jì)被碗。】
ConcreteElement(具體元素):具體元素實(shí)現(xiàn)了accept()方法仿村,在accept()方法中調(diào)用訪問者的訪問方法以便完成對(duì)一個(gè)元素的操作锐朴。
ObjectStructure(對(duì)象結(jié)構(gòu)):對(duì)象結(jié)構(gòu)是一個(gè)元素的集合,它用于存放元素對(duì)象奠宜,并且提供了遍歷其內(nèi)部元素的方法包颁。它可以結(jié)合組合模式來實(shí)現(xiàn),也可以是一個(gè)簡單的集合對(duì)象压真,如一個(gè)List對(duì)象或一個(gè)Set對(duì)象娩嚼。
訪問者模式中對(duì)象結(jié)構(gòu)存儲(chǔ)了不同類型的元素對(duì)象,以供不同訪問者訪問滴肿。訪問者模式包括兩個(gè)層次結(jié)構(gòu)岳悟,一個(gè)是訪問者層次結(jié)構(gòu),提供了抽象訪問者和具體訪問者泼差,一個(gè)是元素層次結(jié)構(gòu)贵少,提供了抽象元素和具體元素。相同的訪問者可以以不同的方式訪問不同的元素堆缘,相同的元素可以接受不同訪問者以不同訪問方式訪問滔灶。在訪問者模式中,增加新的訪問者無須修改原有系統(tǒng)吼肥,系統(tǒng)具有較好的可擴(kuò)展性录平。
在訪問者模式中麻车,抽象訪問者定義了訪問元素對(duì)象的方法,通常為每一種類型的元素對(duì)象都提供一個(gè)訪問方法斗这,而具體訪問者可以實(shí)現(xiàn)這些訪問方法动猬。這些訪問方法的命名一般有兩種方式:一種是直接在方法名中標(biāo)明待訪問元素對(duì)象的具體類型,如visitElementA(ElementA elementA)表箭,還有一種是統(tǒng)一取名為visit()赁咙,通過參數(shù)類型的不同來定義一系列重載的visit()方法。當(dāng)然免钻,如果所有的訪問者對(duì)某一類型的元素的訪問操作都相同彼水,則可以將操作代碼移到抽象訪問者類中,其典型代碼如下所示:
abstract class Visitor
{
public abstract void visit(ConcreteElementA elementA);
public abstract void visit(ConcreteElementB elementB);
public void visit(ConcreteElementC elementC)
{
//元素ConcreteElementC操作代碼
}
}
在這里使用了重載visit()方法的方式來定義多個(gè)方法用于操作不同類型的元素對(duì)象极舔。在抽象訪問者Visitor類的子類ConcreteVisitor中實(shí)現(xiàn)了抽象的訪問方法猿涨,用于定義對(duì)不同類型元素對(duì)象的操作,具體訪問者類典型代碼如下所示:
class ConcreteVisitor extends Visitor
{
public void visit(ConcreteElementA elementA)
{
//元素ConcreteElementA操作代碼
}
public void visit(ConcreteElementB elementB)
{
//元素ConcreteElementB操作代碼
}
}
對(duì)于元素類而言姆怪,在其中一般都定義了一個(gè)accept()方法,用于接受訪問者的訪問澡绩,典型的抽象元素類代碼如下所示:
interface Element
{
public void accept(Visitor visitor);
}
需要注意的是該方法傳入了一個(gè)抽象訪問者Visitor類型的參數(shù)稽揭,即針對(duì)抽象訪問者進(jìn)行編程,而不是具體訪問者肥卡,在程序運(yùn)行時(shí)再確定具體訪問者的類型溪掀,并調(diào)用具體訪問者對(duì)象的visit()方法實(shí)現(xiàn)對(duì)元素對(duì)象的操作。在抽象元素類Element的子類中實(shí)現(xiàn)了accept()方法步鉴,用于接受訪問者的訪問揪胃,在具體元素類中還可以定義不同類型的元素所特有的業(yè)務(wù)方法,其典型代碼如下所示:
class ConcreteElementA implements Element
{
public void accept(Visitor visitor)
{
visitor.visit(this);
}
public void operationA()
{
//業(yè)務(wù)方法
}
}
在具體元素類ConcreteElementA的accept()方法中氛琢,通過調(diào)用Visitor類的visit()方法實(shí)現(xiàn)對(duì)元素的訪問喊递,并以當(dāng)前對(duì)象作為visit()方法的參數(shù)。其具體執(zhí)行過程如下:
(1) 調(diào)用具體元素類的accept(Visitor visitor)方法阳似,并將Visitor子類對(duì)象作為其參數(shù)骚勘;
(2) 在具體元素類accept(Visitor visitor)方法內(nèi)部調(diào)用傳入的Visitor對(duì)象的visit()方法,如visit(ConcreteElementA elementA)撮奏,將當(dāng)前具體元素類對(duì)象(this)作為參數(shù)俏讹,如visitor.visit(this);
(3) 執(zhí)行Visitor對(duì)象的visit()方法畜吊,在其中還可以調(diào)用具體元素對(duì)象的業(yè)務(wù)方法泽疆。
這種調(diào)用機(jī)制也稱為“雙重分派”,正因?yàn)槭褂昧穗p重分派機(jī)制玲献,使得增加新的訪問者無須修改現(xiàn)有類庫代碼殉疼,只需將新的訪問者對(duì)象作為參數(shù)傳入具體元素對(duì)象的accept()方法梯浪,程序運(yùn)行時(shí)將回調(diào)在新增Visitor類中定義的visit()方法,從而增加新的元素訪問方式株依。
思考
雙重分派機(jī)制如何用代碼實(shí)現(xiàn)驱证?
在訪問者模式中,對(duì)象結(jié)構(gòu)是一個(gè)集合恋腕,它用于存儲(chǔ)元素對(duì)象并接受訪問者的訪問抹锄,其典型代碼如下所示:
class ObjectStructure
{
private ArrayList<Element> list = new ArrayList<Element>(); //定義一個(gè)集合用于存儲(chǔ)元素對(duì)象
public void accept(Visitor visitor)
{
Iterator i=list.iterator();
while(i.hasNext())
{
((Element)i.next()).accept(visitor); //遍歷訪問集合中的每一個(gè)元素
}
}
public void addElement(Element element)
{
list.add(element);
}
public void removeElement(Element element)
{
list.remove(element);
}
}
在對(duì)象結(jié)構(gòu)中可以使用迭代器對(duì)存儲(chǔ)在集合中的元素對(duì)象進(jìn)行遍歷,并逐個(gè)調(diào)用每一個(gè)對(duì)象的accept()方法荠藤,實(shí)現(xiàn)對(duì)元素對(duì)象的訪問操作伙单。
思考
訪問者模式是否符合“開閉原則”?【從增加新的訪問者和增加新的元素兩方面考慮哈肖∥怯】
操作復(fù)雜對(duì)象結(jié)構(gòu)——訪問者模式(三)
26.3 完整解決方案
Sunny軟件公司開發(fā)人員使用訪問者模式對(duì)OA系統(tǒng)中員工數(shù)據(jù)匯總模塊進(jìn)行重構(gòu),使得系統(tǒng)可以很方便地增加新類型的訪問者淤井,更加符合“單一職責(zé)原則”和“開閉原則”布疼,重構(gòu)后的基本結(jié)構(gòu)如圖26-3所示:
在圖26-3中,F(xiàn)ADepartment表示財(cái)務(wù)部币狠,HRDepartment表示人力資源部游两,它們充當(dāng)具體訪問者角色,其抽象父類Department充當(dāng)抽象訪問者角色漩绵;EmployeeList充當(dāng)對(duì)象結(jié)構(gòu)贱案,用于存儲(chǔ)員工列表;FulltimeEmployee表示正式員工止吐,ParttimeEmployee表示臨時(shí)工宝踪,它們充當(dāng)具體元素角色,其父接口Employee充當(dāng)抽象元素角色碍扔。完整代碼如下所示:
import java.util.*;
//員工類:抽象元素類
interface Employee
{
public void accept(Department handler); //接受一個(gè)抽象訪問者訪問
}
//全職員工類:具體元素類
class FulltimeEmployee implements Employee
{
private String name;
private double weeklyWage;
private int workTime;
public FulltimeEmployee(String name,double weeklyWage,int workTime)
{
this.name = name;
this.weeklyWage = weeklyWage;
this.workTime = workTime;
}
public void setName(String name)
{
this.name = name;
}
public void setWeeklyWage(double weeklyWage)
{
this.weeklyWage = weeklyWage;
}
public void setWorkTime(int workTime)
{
this.workTime = workTime;
}
public String getName()
{
return (this.name);
}
public double getWeeklyWage()
{
return (this.weeklyWage);
}
public int getWorkTime()
{
return (this.workTime);
}
public void accept(Department handler)
{
handler.visit(this); //調(diào)用訪問者的訪問方法
}
}
//兼職員工類:具體元素類
class ParttimeEmployee implements Employee
{
private String name;
private double hourWage;
private int workTime;
public ParttimeEmployee(String name,double hourWage,int workTime)
{
this.name = name;
this.hourWage = hourWage;
this.workTime = workTime;
}
public void setName(String name)
{
this.name = name;
}
public void setHourWage(double hourWage)
{
this.hourWage = hourWage;
}
public void setWorkTime(int workTime)
{
this.workTime = workTime;
}
public String getName()
{
return (this.name);
}
public double getHourWage()
{
return (this.hourWage);
}
public int getWorkTime()
{
return (this.workTime);
}
public void accept(Department handler)
{
handler.visit(this); //調(diào)用訪問者的訪問方法
}
}
//部門類:抽象訪問者類
abstract class Department
{
//聲明一組重載的訪問方法瘩燥,用于訪問不同類型的具體元素
public abstract void visit(FulltimeEmployee employee);
public abstract void visit(ParttimeEmployee employee);
}
//財(cái)務(wù)部類:具體訪問者類
class FADepartment extends Department
{
//實(shí)現(xiàn)財(cái)務(wù)部對(duì)全職員工的訪問
public void visit(FulltimeEmployee employee)
{
int workTime = employee.getWorkTime();
double weekWage = employee.getWeeklyWage();
if(workTime > 40)
{
weekWage = weekWage + (workTime - 40) * 100;
}
else if(workTime < 40)
{
weekWage = weekWage - (40 - workTime) * 80;
if(weekWage < 0)
{
weekWage = 0;
}
}
System.out.println("正式員工" + employee.getName() + "實(shí)際工資為:" + weekWage + "元。");
}
//實(shí)現(xiàn)財(cái)務(wù)部對(duì)兼職員工的訪問
public void visit(ParttimeEmployee employee)
{
int workTime = employee.getWorkTime();
double hourWage = employee.getHourWage();
System.out.println("臨時(shí)工" + employee.getName() + "實(shí)際工資為:" + workTime * hourWage + "元不同。");
}
}
//人力資源部類:具體訪問者類
class HRDepartment extends Department
{
//實(shí)現(xiàn)人力資源部對(duì)全職員工的訪問
public void visit(FulltimeEmployee employee)
{
int workTime = employee.getWorkTime();
System.out.println("正式員工" + employee.getName() + "實(shí)際工作時(shí)間為:" + workTime + "小時(shí)颤芬。");
if(workTime > 40)
{
System.out.println("正式員工" + employee.getName() + "加班時(shí)間為:" + (workTime - 40) + "小時(shí)。");
}
else if(workTime < 40)
{
System.out.println("正式員工" + employee.getName() + "請(qǐng)假時(shí)間為:" + (40 - workTime) + "小時(shí)套鹅。");
}
}
//實(shí)現(xiàn)人力資源部對(duì)兼職員工的訪問
public void visit(ParttimeEmployee employee)
{
int workTime = employee.getWorkTime();
System.out.println("臨時(shí)工" + employee.getName() + "實(shí)際工作時(shí)間為:" + workTime + "小時(shí)站蝠。");
}
}
//員工列表類:對(duì)象結(jié)構(gòu)
class EmployeeList
{
//定義一個(gè)集合用于存儲(chǔ)員工對(duì)象
private ArrayList<Employee> list = new ArrayList<Employee>();
public void addEmployee(Employee employee)
{
list.add(employee);
}
//遍歷訪問員工集合中的每一個(gè)員工對(duì)象
public void accept(Department handler)
{
for(Object obj : list)
{
((Employee)obj).accept(handler);
}
}
}
為了提高系統(tǒng)的靈活性和可擴(kuò)展性,我們將具體訪問者類的類名存儲(chǔ)在配置文件中卓鹿,并通過工具類XMLUtil來讀取配置文件并反射生成對(duì)象菱魔,XMLUtil類的代碼如下所示:
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import java.io.*;
class XMLUtil
{
//該方法用于從XML配置文件中提取具體類類名,并返回一個(gè)實(shí)例對(duì)象
public static Object getBean()
{
try
{
//創(chuàng)建文檔對(duì)象
DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dFactory.newDocumentBuilder();
Document doc;
doc = builder.parse(new File("config.xml"));
//獲取包含類名的文本節(jié)點(diǎn)
NodeList nl = doc.getElementsByTagName("className");
Node classNode=nl.item(0).getFirstChild();
String cName=classNode.getNodeValue();
//通過類名生成實(shí)例對(duì)象并將其返回
Class c=Class.forName(cName);
Object obj=c.newInstance();
return obj;
}
catch(Exception e)
{
e.printStackTrace();
return null;
}
}
}
配置文件config.xml中存儲(chǔ)了具體訪問者類的類名吟孙,代碼如下所示:
<?xml version="1.0"?>
<config>
<className>FADepartment</className>
</config>
編寫如下客戶端測(cè)試代碼:
class Client
{
public static void main(String args[])
{
EmployeeList list = new EmployeeList();
Employee fte1,fte2,fte3,pte1,pte2;
fte1 = new FulltimeEmployee("張無忌",3200.00,45);
fte2 = new FulltimeEmployee("楊過",2000.00,40);
fte3 = new FulltimeEmployee("段譽(yù)",2400.00,38);
pte1 = new ParttimeEmployee("洪七公",80.00,20);
pte2 = new ParttimeEmployee("郭靖",60.00,18);
list.addEmployee(fte1);
list.addEmployee(fte2);
list.addEmployee(fte3);
list.addEmployee(pte1);
list.addEmployee(pte2);
Department dep;
dep = (Department)XMLUtil.getBean();
list.accept(dep);
}
}
編譯并運(yùn)行程序澜倦,輸出結(jié)果如下:
正式員工張無忌實(shí)際工資為:3700.0元聚蝶。
正式員工楊過實(shí)際工資為:2000.0元。
正式員工段譽(yù)實(shí)際工資為:2240.0元藻治。
臨時(shí)工洪七公實(shí)際工資為:1600.0元碘勉。
臨時(shí)工郭靖實(shí)際工資為:1080.0元。
如果需要更換具體訪問者類桩卵,無須修改源代碼验靡,只需修改配置文件,例如將訪問者類由財(cái)務(wù)部改為人力資源部雏节,只需將存儲(chǔ)在配置文件中的具體訪問者類FADepartment改為HRDepartment胜嗓,如下代碼所示:
<?xml version="1.0"?>
<config>
<className>HRDepartment</className>
</config>
重新運(yùn)行客戶端程序,輸出結(jié)果如下:
正式員工張無忌實(shí)際工作時(shí)間為:45小時(shí)钩乍。
正式員工張無忌加班時(shí)間為:5小時(shí)辞州。
正式員工楊過實(shí)際工作時(shí)間為:40小時(shí)。
正式員工段譽(yù)實(shí)際工作時(shí)間為:38小時(shí)寥粹。
正式員工段譽(yù)請(qǐng)假時(shí)間為:2小時(shí)变过。
臨時(shí)工洪七公實(shí)際工作時(shí)間為:20小時(shí)。
臨時(shí)工郭靖實(shí)際工作時(shí)間為:18小時(shí)涝涤。
如果要在系統(tǒng)中增加一種新的訪問者牵啦,無須修改源代碼,只要增加一個(gè)新的具體訪問者類即可妄痪,在該具體訪問者中封裝了新的操作元素對(duì)象的方法。從增加新的訪問者的角度來看楞件,訪問者模式符合“開閉原則”衫生。
如果要在系統(tǒng)中增加一種新的具體元素,例如增加一種新的員工類型為“退休人員”土浸,由于原有系統(tǒng)并未提供相應(yīng)的訪問接口(在抽象訪問者中沒有聲明任何訪問“退休人員”的方法)罪针,因此必須對(duì)原有系統(tǒng)進(jìn)行修改,在原有的抽象訪問者類和具體訪問者類中增加相應(yīng)的訪問方法黄伊。從增加新的元素的角度來看泪酱,訪問者模式違背了“開閉原則”。
綜上所述还最,訪問者模式與抽象工廠模式類似墓阀,對(duì)“開閉原則”的支持具有傾斜性,可以很方便地添加新的訪問者拓轻,但是添加新的元素較為麻煩斯撮。
操作復(fù)雜對(duì)象結(jié)構(gòu)——訪問者模式(四)
26.4 訪問者模式與組合模式聯(lián)用
在訪問者模式中,包含一個(gè)用于存儲(chǔ)元素對(duì)象集合的對(duì)象結(jié)構(gòu)扶叉,我們通澄鸸可以使用迭代器來遍歷對(duì)象結(jié)構(gòu)帕膜,同時(shí)具體元素之間可以存在整體與部分關(guān)系,有些元素作為容器對(duì)象溢十,有些元素作為成員對(duì)象垮刹,可以使用組合模式來組織元素。引入組合模式后的訪問者模式結(jié)構(gòu)圖如圖26-4所示:
需要注意的是张弛,在圖26-4所示結(jié)構(gòu)中荒典,由于葉子元素的遍歷操作已經(jīng)在容器元素中完成,因此要防止單獨(dú)將已增加到容器元素中的葉子元素再次加入對(duì)象結(jié)構(gòu)中乌庶,對(duì)象結(jié)構(gòu)中只保存容器元素和孤立的葉子元素种蝶。
26.5 訪問者模式總結(jié)
由于訪問者模式的使用條件較為苛刻,本身結(jié)構(gòu)也較為復(fù)雜瞒大,因此在實(shí)際應(yīng)用中使用頻率不是特別高螃征。當(dāng)系統(tǒng)中存在一個(gè)較為復(fù)雜的對(duì)象結(jié)構(gòu),且不同訪問者對(duì)其所采取的操作也不相同時(shí)透敌,可以考慮使用訪問者模式進(jìn)行設(shè)計(jì)盯滚。在XML文檔解析、編譯器的設(shè)計(jì)酗电、復(fù)雜集合對(duì)象的處理等領(lǐng)域訪問者模式得到了一定的應(yīng)用魄藕。
1.主要優(yōu)點(diǎn)
訪問者模式的主要優(yōu)點(diǎn)如下:
(1) 增加新的訪問操作很方便。使用訪問者模式撵术,增加新的訪問操作就意味著增加一個(gè)新的具體訪問者類背率,實(shí)現(xiàn)簡單,無須修改源代碼嫩与,符合“開閉原則”寝姿。
(2) 將有關(guān)元素對(duì)象的訪問行為集中到一個(gè)訪問者對(duì)象中,而不是分散在一個(gè)個(gè)的元素類中划滋。類的職責(zé)更加清晰饵筑,有利于對(duì)象結(jié)構(gòu)中元素對(duì)象的復(fù)用,相同的對(duì)象結(jié)構(gòu)可以供多個(gè)不同的訪問者訪問处坪。
(3) 讓用戶能夠在不修改現(xiàn)有元素類層次結(jié)構(gòu)的情況下根资,定義作用于該層次結(jié)構(gòu)的操作。
2.主要缺點(diǎn)
訪問者模式的主要缺點(diǎn)如下:
(1) 增加新的元素類很困難同窘。在訪問者模式中玄帕,每增加一個(gè)新的元素類都意味著要在抽象訪問者角色中增加一個(gè)新的抽象操作,并在每一個(gè)具體訪問者類中增加相應(yīng)的具體操作想邦,這違背了“開閉原則”的要求桨仿。
(2) 破壞封裝。訪問者模式要求訪問者對(duì)象訪問并調(diào)用每一個(gè)元素對(duì)象的操作案狠,這意味著元素對(duì)象有時(shí)候必須暴露一些自己的內(nèi)部操作和內(nèi)部狀態(tài)服傍,否則無法供訪問者訪問钱雷。
3.適用場(chǎng)景
在以下情況下可以考慮使用訪問者模式:
(1) 一個(gè)對(duì)象結(jié)構(gòu)包含多個(gè)類型的對(duì)象,希望對(duì)這些對(duì)象實(shí)施一些依賴其具體類型的操作吹零。在訪問者中針對(duì)每一種具體的類型都提供了一個(gè)訪問操作罩抗,不同類型的對(duì)象可以有不同的訪問操作。
(2) 需要對(duì)一個(gè)對(duì)象結(jié)構(gòu)中的對(duì)象進(jìn)行很多不同的并且不相關(guān)的操作灿椅,而需要避免讓這些操作“污染”這些對(duì)象的類套蒂,也不希望在增加新操作時(shí)修改這些類。訪問者模式使得我們可以將相關(guān)的訪問操作集中起來定義在訪問者類中茫蛹,對(duì)象結(jié)構(gòu)可以被多個(gè)不同的訪問者類所使用操刀,將對(duì)象本身與對(duì)象的訪問操作分離。
(3) 對(duì)象結(jié)構(gòu)中對(duì)象對(duì)應(yīng)的類很少改變婴洼,但經(jīng)常需要在此對(duì)象結(jié)構(gòu)上定義新的操作骨坑。
練習(xí)
Sunny軟件公司欲為某高校開發(fā)一套獎(jiǎng)勵(lì)審批系統(tǒng),該系統(tǒng)可以實(shí)現(xiàn)教師獎(jiǎng)勵(lì)和學(xué)生獎(jiǎng)勵(lì)的審批(Award Check)柬采,如果教師發(fā)表論文數(shù)超過10篇或者學(xué)生論文超過2篇可以評(píng)選科研獎(jiǎng)欢唾,如果教師教學(xué)反饋分大于等于90分或者學(xué)生平均成績大于等于90分可以評(píng)選成績優(yōu)秀獎(jiǎng)。試使用訪問者模式設(shè)計(jì)該系統(tǒng)粉捻,以判斷候選人集合中的教師或?qū)W生是否符合某種獲獎(jiǎng)要求礁遣。
練習(xí)會(huì)在我的github上做掉