訪問者模式
案例
張三所在公司欲為某高校開發(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é)生平均成績(jī)大于等于90分可以評(píng)選成績(jī)優(yōu)秀獎(jiǎng)。該系統(tǒng)主要用于判斷候選人集合中的教師或?qū)W生是否符合某種獲獎(jiǎng)要求。張三想了想就開始動(dòng)手寫起來了。
1.首先他定義了一個(gè)父類:
// 父類,主要存放一些公共字段
public class Person {
// 姓名
private String name;
// 論文數(shù)
private int paperNums;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPaperNums() {
return paperNums;
}
public void setPaperNums(int paperNums) {
this.paperNums = paperNums;
}
}
2.然后分別是兩個(gè)實(shí)體類:
老師類:
// 老師類
public class Teacher extends Person {
// 教學(xué)反饋分
private int feedbackScore;
public Teacher(String name, int paperNums, int feedbackScore) {
this.setName(name);
this.setPaperNums(paperNums);
this.feedbackScore = feedbackScore;
}
public int getFeedbackScore() {
return feedbackScore;
}
public void setFeedbackScore(int feedbackScore) {
this.feedbackScore = feedbackScore;
}
}
學(xué)生類:
// 學(xué)生類
public class Student extends Person {
// 平均成績(jī)
private int averageScore;
public Student(String name, int paperNums, int averageScore) {
this.setName(name);
this.setPaperNums(paperNums);
this.averageScore = averageScore;
}
public int getAverageScore() {
return averageScore;
}
public void setAverageScore(int averageScore) {
this.averageScore = averageScore;
}
}
3.獎(jiǎng)勵(lì)審批系統(tǒng)關(guān)鍵代碼:
// 獎(jiǎng)勵(lì)審批系統(tǒng)
public class AwardCheckSystem {
// 存放元素的容器
private List<Person> personList = new ArrayList<>();
// 添加元素方法
public void addPerson(Person person) {
personList.add(person);
}
// 系統(tǒng)判斷評(píng)選資格核心代碼
public void awardCheck(String prize) {
if (prize.equals("research")) {
for (Person person : personList) {
int paperNums = person.getPaperNums();
if (person instanceof Teacher && paperNums > 10) {
System.out.println(person.getName() + "老師發(fā)表論文數(shù)為:" + paperNums + ",擁有評(píng)選科研獎(jiǎng)資格");
} else if (person instanceof Student && paperNums > 2) {
System.out.println(person.getName() + "同學(xué)發(fā)表論文數(shù)為:" + paperNums + ",擁有評(píng)選科研獎(jiǎng)資格");
}
}
} else if (prize.equals("excellent")) {
for (Person person : personList) {
if (person instanceof Teacher && ((Teacher) person).getFeedbackScore() >= 90) {
System.out.println(person.getName() + "老師發(fā)表教學(xué)反饋分為:" + ((Teacher) person).getFeedbackScore() + ",擁有評(píng)選成績(jī)優(yōu)秀獎(jiǎng)資格");
} else if (person instanceof Student && ((Student) person).getAverageScore() >= 90) {
System.out.println(person.getName() + "同學(xué)平均成績(jī)?yōu)椋? + ((Student) person).getAverageScore() + ",擁有評(píng)選成績(jī)優(yōu)秀獎(jiǎng)資格");
}
}
}
}
}
4.客戶端使用:
public class Main {
public static void main(String[] args) {
AwardCheckSystem awardCheckSystem = new AwardCheckSystem();
awardCheckSystem.addPerson(new Teacher("張三", 9, 91));
awardCheckSystem.addPerson(new Teacher("李四", 11, 89));
awardCheckSystem.addPerson(new Student("王五", 1, 92));
awardCheckSystem.addPerson(new Student("趙六", 3, 88));
System.out.println("擁有評(píng)選科研獎(jiǎng)資格的人有:");
awardCheckSystem.awardCheck("research");
System.out.println("----------------------------------------------");
System.out.println("擁有評(píng)選成績(jī)優(yōu)秀獎(jiǎng)資格的人有:");
awardCheckSystem.awardCheck("excellent");
}
}
5.使用結(jié)果:
擁有評(píng)選科研獎(jiǎng)資格的人有:
李四老師發(fā)表論文數(shù)為:11,擁有評(píng)選科研獎(jiǎng)資格
趙六同學(xué)發(fā)表論文數(shù)為:3,擁有評(píng)選科研獎(jiǎng)資格
----------------------------------------------
擁有評(píng)選成績(jī)優(yōu)秀獎(jiǎng)資格的人有:
張三老師發(fā)表教學(xué)反饋分為:91,擁有評(píng)選成績(jī)優(yōu)秀獎(jiǎng)資格
王五同學(xué)平均成績(jī)?yōu)椋?2,擁有評(píng)選成績(jī)優(yōu)秀獎(jiǎng)資格
張三很快就寫出了獎(jiǎng)勵(lì)審批系統(tǒng)中最核心的代碼,但是他覺得在awardCheck()方法中通過獎(jiǎng)項(xiàng)名稱和人員類型判斷是否有資格評(píng)選獎(jiǎng)項(xiàng)的代碼看上去很是復(fù)雜,他想要改進(jìn)一下。剛好在設(shè)計(jì)模式中對(duì)于這種集合對(duì)象中存在多種不同元素,同時(shí)對(duì)于這些不同元素不同的處理者會(huì)有不同的處理方式的情況可以使用訪問者模式對(duì)其進(jìn)行改進(jìn)。
模式介紹
訪問者模式(Visitor Pattern):提供一個(gè)作用于某對(duì)象結(jié)構(gòu)中的各元素的操作表示,它使我們可以在不改變各元素的類的前提下定義作用于這些元素的新操作。訪問者模式是一種對(duì)象行為型模式。
角色構(gòu)成
- 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ù)?!旧院髮⒔榻B為什么要這樣設(shè)計(jì)?!?/li>
- 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è)簡(jiǎn)單的集合對(duì)象,如一個(gè)List對(duì)象或一個(gè)Set對(duì)象。
UML 類圖

訪問者模式是一種較為復(fù)雜的行為型設(shè)計(jì)模式,它不是那么容易理解的,這里再描述一下訪問者模式的定義以及上面幾個(gè)角色作用。
首先它是作用于某對(duì)象結(jié)構(gòu)中的各元素的操作的,具體表現(xiàn)就是
ObjectStructure(對(duì)象結(jié)構(gòu))保存了Element抽象元素中的各個(gè)ConcreteElement具體元素,并提供遍歷操作各個(gè)具體元素的方法,類圖中為accept()方法。抽象元素
Element中定義了accept(Visitor visitor)方法,用于接受訪問者訪問的方法,并在具體元素類ConcreteElement中的具體方法中調(diào)用訪問者的方法同時(shí)將具體元素作為參數(shù)傳遞個(gè)訪問者。抽象訪問者中定義了訪問不同元素的接口方法,便于對(duì)象結(jié)構(gòu)
ObjectStructure類中方法的調(diào)用,同時(shí)具體訪問者完成訪問不同元素的具體實(shí)現(xiàn)代碼。
這樣就構(gòu)成了訪問者模式在不改變各元素的類的前提下定義作用于這些元素的新操作。
代碼改造
1.首先是抽象元素與具體元素類:
抽象父類:
// 父類,主要存放一些公共字段
public abstract class Person {
// 定義用于訪問者訪問的方法
public abstract void accept(Award award);
}
老師類:
// 老師類
public class Teacher extends Person {
// 實(shí)現(xiàn)訪問者訪問元素的方法
@Override
public void accept(Award award) {
award.visit(this);
}
}
學(xué)生類:
// 學(xué)生類
public class Student extends Person {
// 實(shí)現(xiàn)訪問者訪問元素的方法
@Override
public void accept(Award award) {
award.visit(this);
}
}
這三個(gè)類相較于改造前的類主要是多了accept(Award award)方法,其他代碼完全一樣,因此省略了重復(fù)代碼。
2.抽象訪問者類:
// 抽象訪問者,定義訪問具體元素的方法
public interface Award {
// 提供訪問老師類接口
void visit(Teacher person);
// 提供訪問學(xué)生類接口
void visit(Student person);
}
3.兩個(gè)具體訪問者類:
科研獎(jiǎng)資格判斷類:
// 科研獎(jiǎng)資格判斷類(具體訪問者類角色)
public class ResearchAward implements Award {
@Override
public void visit(Teacher person) {
int paperNums = person.getPaperNums();
if (paperNums > 10) {
System.out.println(person.getName() + "老師發(fā)表論文數(shù)為:" + paperNums + ",擁有評(píng)選科研獎(jiǎng)資格");
}
}
@Override
public void visit(Student person) {
int paperNums = person.getPaperNums();
if (paperNums > 2) {
System.out.println(person.getName() + "同學(xué)發(fā)表論文數(shù)為:" + paperNums + ",擁有評(píng)選科研獎(jiǎng)資格");
}
}
}
成績(jī)優(yōu)秀獎(jiǎng)資格判斷類:
// 成績(jī)優(yōu)秀獎(jiǎng)資格判斷類(具體訪問者類)
public class ExcellentAward implements Award {
@Override
public void visit(Teacher person) {
if (person.getFeedbackScore() >= 90) {
System.out.println(person.getName() + "老師發(fā)表教學(xué)反饋分為:" + person.getFeedbackScore() + ",擁有評(píng)選成績(jī)優(yōu)秀獎(jiǎng)資格");
}
}
@Override
public void visit(Student person) {
if (person.getAverageScore() >= 90) {
System.out.println(person.getName() + "同學(xué)平均成績(jī)?yōu)椋? + person.getAverageScore() + ",擁有評(píng)選成績(jī)優(yōu)秀獎(jiǎng)資格");
}
}
}
4.對(duì)象結(jié)構(gòu)類:
// 獎(jiǎng)勵(lì)審批系統(tǒng)(對(duì)象結(jié)構(gòu)類角色)
public class AwardCheckSystem {
// 存放元素的容器
private List<Person> personList = new ArrayList<>();
// 添加元素方法
public void addPerson(Person person) {
personList.add(person);
}
// 系統(tǒng)判斷評(píng)選資格核心代碼
public void awardCheck(Award award) {
for (Person person : personList) {
person.accept(award);
}
}
}
5.客戶端使用:
public class Main {
public static void main(String[] args) {
AwardCheckSystem awardCheckSystem = new AwardCheckSystem();
awardCheckSystem.addPerson(new Teacher("張三", 9, 91));
awardCheckSystem.addPerson(new Teacher("李四", 11, 89));
awardCheckSystem.addPerson(new Student("王五", 1, 92));
awardCheckSystem.addPerson(new Student("趙六", 3, 88));
System.out.println("擁有評(píng)選科研獎(jiǎng)資格的人有:");
awardCheckSystem.awardCheck(new ResearchAward());
System.out.println("----------------------------------------------");
System.out.println("擁有評(píng)選成績(jī)優(yōu)秀獎(jiǎng)資格的人有:");
awardCheckSystem.awardCheck(new ExcellentAward());
}
}
擁有評(píng)選科研獎(jiǎng)資格的人有:
李四老師發(fā)表論文數(shù)為:11,擁有評(píng)選科研獎(jiǎng)資格
趙六同學(xué)發(fā)表論文數(shù)為:3,擁有評(píng)選科研獎(jiǎng)資格
----------------------------------------------
擁有評(píng)選成績(jī)優(yōu)秀獎(jiǎng)資格的人有:
張三老師發(fā)表教學(xué)反饋分為:91,擁有評(píng)選成績(jī)優(yōu)秀獎(jiǎng)資格
王五同學(xué)平均成績(jī)?yōu)椋?2,擁有評(píng)選成績(jī)優(yōu)秀獎(jiǎng)資格
經(jīng)過改造之后輸出結(jié)果和上面的一摸一樣,但獎(jiǎng)勵(lì)審批系統(tǒng)判斷獎(jiǎng)項(xiàng)評(píng)選資格的核心代碼變得非常簡(jiǎn)潔。同時(shí)如果要有其他的獎(jiǎng)項(xiàng)資格判斷,只需要增加一個(gè)新的具體訪問者類并在新的獎(jiǎng)項(xiàng)資格判斷類中添加具體的判斷邏輯就可以了,大大提高了系統(tǒng)的可擴(kuò)展性。
訪問者模式也有一個(gè)很是明顯的問題,它在添加新的訪問者的時(shí)候是很容易的,但在添加新的元素時(shí)較為麻煩。在這個(gè)獎(jiǎng)項(xiàng)審批系統(tǒng)案例里面因?yàn)樾枨缶褪桥袛嗬蠋熀蛯W(xué)生是否有評(píng)獎(jiǎng)資格,涉及到的元素只有老師和學(xué)生,應(yīng)該也不會(huì)出現(xiàn)變化,但是還有可能評(píng)選其他獎(jiǎng)項(xiàng)的資格,所以這里用訪問者模式是很合適的。
模式應(yīng)用
訪問者模式在合適的場(chǎng)景下使用之后,會(huì)使代碼變得更加靈活易于擴(kuò)展。下面通過介紹它在 Spring 中的具體應(yīng)用,讓我們對(duì)模式的應(yīng)用更加深刻。
1.首先是 pom.xml 文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>design-pattern</artifactId>
<groupId>com.phoegel</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>vistor</artifactId>
<properties>
<spring.version>5.1.15.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
</project>
2.簡(jiǎn)單的定義一個(gè)實(shí)體類:
public class Person {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
3.然后是 spring 配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="classpath:person.properties"/>
<bean id="person" class="com.phoegel.visitor.analysis.Person" scope="prototype">
<property name="name" value="${person.name}"/>
<property name="age" value="${person.age}"/>
</bean>
</beans>
4.這里使用占位符的方式初始化Person類實(shí)例,因此配置一個(gè)person.properties文件:
person.name=張三
person.age=18
5.然后簡(jiǎn)單的使用:
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Person person = (Person) context.getBean("person");
System.out.println(person);
}
}
6.使用結(jié)果:
Person{name='張三', age=18}
這里只是簡(jiǎn)單的輸出了初始化對(duì)象的信息。重點(diǎn)是想要說明的這里使用占位符${}的方式將配置文件person.properties的信息設(shè)置到對(duì)象字段里面,在 Spring 中是通過PropertySourcesPlaceholderConfigurer類中的processProperties()方法中完成的,而方法內(nèi)部又調(diào)用了PlaceholderConfigurerSupport類中的doProcessProperties()方法,在doProcessProperties內(nèi)部就使用到了BeanDefinitionVisitor類,這個(gè)類就代表了訪問者類。通過追蹤源碼可以下面的關(guān)鍵代碼:
protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
StringValueResolver valueResolver) {
// 通過 BeanDefinitionVisitor 類的 visitBeanDefinition() 方法來實(shí)現(xiàn)訪問者模式的核心思想
BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver);
String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames();
for (String curName : beanNames) {
if (!(curName.equals(this.beanName) && beanFactoryToProcess.equals(this.beanFactory))) {
BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName);
try {
visitor.visitBeanDefinition(bd);
}
catch (Exception ex) {
throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage(), ex);
}
}
}
beanFactoryToProcess.resolveAliases(valueResolver);
beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
}
其中BeanDefinitionVisitor類的visitBeanDefinition()方法如下:
public void visitBeanDefinition(BeanDefinition beanDefinition) {
visitParentName(beanDefinition);
visitBeanClassName(beanDefinition);
visitFactoryBeanName(beanDefinition);
visitFactoryMethodName(beanDefinition);
visitScope(beanDefinition);
if (beanDefinition.hasPropertyValues()) {
visitPropertyValues(beanDefinition.getPropertyValues());
}
if (beanDefinition.hasConstructorArgumentValues()) {
ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
visitIndexedArgumentValues(cas.getIndexedArgumentValues());
visitGenericArgumentValues(cas.getGenericArgumentValues());
}
}
觀察visitBeanDefinition()方法的方法簽名,可以發(fā)現(xiàn)BeanDefinition是一個(gè)接口,也就是訪問者模式中的抽象元素角色,而它的子類有RootBeanDefinition、ChildBeanDefinition和GenericBeanDefinition等等,這些可以理解為具體的元素角色。需要注意的是,這里的BeanDefinition明顯是一個(gè)實(shí)現(xiàn)類,也就是說在 Spring 中并沒有抽象出抽象訪問者來對(duì)具體訪問者類進(jìn)行擴(kuò)展,但是訪問者模式的思想在上面幾個(gè)類之間的運(yùn)用得到了充分的體現(xiàn)。
總結(jié)
主要優(yōu)點(diǎn)
- 增加新的訪問操作很方便。使用訪問者模式,增加新的訪問操作就意味著增加一個(gè)新的具體訪問者類,實(shí)現(xiàn)簡(jiǎn)單,無須修改源代碼,符合“開閉原則”。
- 將有關(guān)元素對(duì)象的訪問行為集中到一個(gè)訪問者對(duì)象中,而不是分散在一個(gè)個(gè)的元素類中。類的職責(zé)更加清晰,有利于對(duì)象結(jié)構(gòu)中元素對(duì)象的復(fù)用,相同的對(duì)象結(jié)構(gòu)可以供多個(gè)不同的訪問者訪問。
- 讓用戶能夠在不修改現(xiàn)有元素類層次結(jié)構(gòu)的情況下,定義作用于該層次結(jié)構(gòu)的操作。
主要缺點(diǎn)
- 增加新的元素類很困難。在訪問者模式中,每增加一個(gè)新的元素類都意味著要在抽象訪問者角色中增加一個(gè)新的抽象操作,并在每一個(gè)具體訪問者類中增加相應(yīng)的具體操作,這違背了“開閉原則”的要求。
- 破壞封裝。訪問者模式要求訪問者對(duì)象訪問并調(diào)用每一個(gè)元素對(duì)象的操作,這意味著元素對(duì)象有時(shí)候必須暴露一些自己的內(nèi)部操作和內(nèi)部狀態(tài),否則無法供訪問者訪問。
適用場(chǎng)景
- 一個(gè)對(duì)象結(jié)構(gòu)包含多個(gè)類型的對(duì)象,希望對(duì)這些對(duì)象實(shí)施一些依賴其具體類型的操作。在訪問者中針對(duì)每一種具體的類型都提供了一個(gè)訪問操作,不同類型的對(duì)象可以有不同的訪問操作。
- 需要對(duì)一個(gè)對(duì)象結(jié)構(gòu)中的對(duì)象進(jìn)行很多不同的并且不相關(guān)的操作,而需要避免讓這些操作“污染”這些對(duì)象的類,也不希望在增加新操作時(shí)修改這些類。訪問者模式使得我們可以將相關(guān)的訪問操作集中起來定義在訪問者類中,對(duì)象結(jié)構(gòu)可以被多個(gè)不同的訪問者類所使用,將對(duì)象本身與對(duì)象的訪問操作分離。
- 對(duì)象結(jié)構(gòu)中對(duì)象對(duì)應(yīng)的類很少改變,但經(jīng)常需要在此對(duì)象結(jié)構(gòu)上定義新的操作。
參考資料
- 大話設(shè)計(jì)模式
- 設(shè)計(jì)模式Java版本-劉偉
- 設(shè)計(jì)模式深入淺出--24.訪問者模式簡(jiǎn)單實(shí)例及其在JDK、Spring中的應(yīng)用
本篇文章github代碼地址:https://github.com/Phoegel/design-pattern/tree/main/visitor
轉(zhuǎn)載請(qǐng)說明出處,本篇博客地址:http://www.itdecent.cn/p/875a0a822fd1