訪問者模式
案例
張三所在公司欲為某高校開發(fā)一套獎勵審批系統(tǒng)惹谐,該系統(tǒng)可以實現教師獎勵和學生獎勵的審批(Award Check),如果教師發(fā)表論文數超過10篇或者學生論文超過2篇可以評選科研獎,如果教師教學反饋分大于等于90分或者學生平均成績大于等于90分可以評選成績優(yōu)秀獎窟哺。該系統(tǒng)主要用于判斷候選人集合中的教師或學生是否符合某種獲獎要求悠反。張三想了想就開始動手寫起來了。
1.首先他定義了一個父類:
// 父類寝优,主要存放一些公共字段
public class Person {
// 姓名
private String name;
// 論文數
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.然后分別是兩個實體類:
老師類:
// 老師類
public class Teacher extends Person {
// 教學反饋分
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;
}
}
學生類:
// 學生類
public class Student extends Person {
// 平均成績
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.獎勵審批系統(tǒng)關鍵代碼:
// 獎勵審批系統(tǒng)
public class AwardCheckSystem {
// 存放元素的容器
private List<Person> personList = new ArrayList<>();
// 添加元素方法
public void addPerson(Person person) {
personList.add(person);
}
// 系統(tǒ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ā)表論文數為:" + paperNums + ",擁有評選科研獎資格");
} else if (person instanceof Student && paperNums > 2) {
System.out.println(person.getName() + "同學發(fā)表論文數為:" + paperNums + "枫耳,擁有評選科研獎資格");
}
}
} else if (prize.equals("excellent")) {
for (Person person : personList) {
if (person instanceof Teacher && ((Teacher) person).getFeedbackScore() >= 90) {
System.out.println(person.getName() + "老師發(fā)表教學反饋分為:" + ((Teacher) person).getFeedbackScore() + "乏矾,擁有評選成績優(yōu)秀獎資格");
} else if (person instanceof Student && ((Student) person).getAverageScore() >= 90) {
System.out.println(person.getName() + "同學平均成績?yōu)椋? + ((Student) person).getAverageScore() + ",擁有評選成績優(yōu)秀獎資格");
}
}
}
}
}
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("擁有評選科研獎資格的人有:");
awardCheckSystem.awardCheck("research");
System.out.println("----------------------------------------------");
System.out.println("擁有評選成績優(yōu)秀獎資格的人有:");
awardCheckSystem.awardCheck("excellent");
}
}
5.使用結果:
擁有評選科研獎資格的人有:
李四老師發(fā)表論文數為:11迁杨,擁有評選科研獎資格
趙六同學發(fā)表論文數為:3钻心,擁有評選科研獎資格
----------------------------------------------
擁有評選成績優(yōu)秀獎資格的人有:
張三老師發(fā)表教學反饋分為:91,擁有評選成績優(yōu)秀獎資格
王五同學平均成績?yōu)椋?2铅协,擁有評選成績優(yōu)秀獎資格
張三很快就寫出了獎勵審批系統(tǒng)中最核心的代碼捷沸,但是他覺得在awardCheck()
方法中通過獎項名稱和人員類型判斷是否有資格評選獎項的代碼看上去很是復雜,他想要改進一下狐史。剛好在設計模式中對于這種集合對象中存在多種不同元素痒给,同時對于這些不同元素不同的處理者會有不同的處理方式的情況可以使用訪問者模式對其進行改進。
模式介紹
訪問者模式(Visitor Pattern):提供一個作用于某對象結構中的各元素的操作表示骏全,它使我們可以在不改變各元素的類的前提下定義作用于這些元素的新操作苍柏。訪問者模式是一種對象行為型模式。
角色構成
- Vistor(抽象訪問者):抽象訪問者為對象結構中每一個具體元素類ConcreteElement聲明一個訪問操作姜贡,從這個操作的名稱或參數類型可以清楚知道需要訪問的具體元素的類型试吁,具體訪問者需要實現這些操作方法,定義對這些元素的訪問操作楼咳。
- ConcreteVisitor(具體訪問者):具體訪問者實現了每個由抽象訪問者聲明的操作熄捍,每一個操作用于訪問對象結構中一種類型的元素。
- Element(抽象元素):抽象元素一般是抽象類或者接口母怜,它定義一個accept()方法余耽,該方法通常以一個抽象訪問者作為參數〔谏辏【稍后將介紹為什么要這樣設計宾添〈遥】
- ConcreteElement(具體元素):具體元素實現了accept()方法,在accept()方法中調用訪問者的訪問方法以便完成對一個元素的操作缕陕。
- ObjectStructure(對象結構):對象結構是一個元素的集合粱锐,它用于存放元素對象,并且提供了遍歷其內部元素的方法扛邑。它可以結合組合模式來實現怜浅,也可以是一個簡單的集合對象,如一個List對象或一個Set對象蔬崩。
UML 類圖
訪問者模式是一種較為復雜的行為型設計模式恶座,它不是那么容易理解的,這里再描述一下訪問者模式的定義以及上面幾個角色作用沥阳。
首先它是作用于某對象結構中的各元素的操作的跨琳,具體表現就是
ObjectStructure(對象結構)
保存了Element
抽象元素中的各個ConcreteElement
具體元素,并提供遍歷操作各個具體元素的方法桐罕,類圖中為accept()
方法脉让。抽象元素
Element
中定義了accept(Visitor visitor)
方法瘾晃,用于接受訪問者訪問的方法刁赦,并在具體元素類ConcreteElement
中的具體方法中調用訪問者的方法同時將具體元素作為參數傳遞個訪問者。抽象訪問者中定義了訪問不同元素的接口方法抑诸,便于對象結構
ObjectStructure
類中方法的調用薪伏,同時具體訪問者完成訪問不同元素的具體實現代碼滚澜。
這樣就構成了訪問者模式在不改變各元素的類的前提下定義作用于這些元素的新操作。
代碼改造
1.首先是抽象元素與具體元素類:
抽象父類:
// 父類嫁怀,主要存放一些公共字段
public abstract class Person {
// 定義用于訪問者訪問的方法
public abstract void accept(Award award);
}
老師類:
// 老師類
public class Teacher extends Person {
// 實現訪問者訪問元素的方法
@Override
public void accept(Award award) {
award.visit(this);
}
}
學生類:
// 學生類
public class Student extends Person {
// 實現訪問者訪問元素的方法
@Override
public void accept(Award award) {
award.visit(this);
}
}
這三個類相較于改造前的類主要是多了accept(Award award)
方法设捐,其他代碼完全一樣,因此省略了重復代碼眶掌。
2.抽象訪問者類:
// 抽象訪問者挡育,定義訪問具體元素的方法
public interface Award {
// 提供訪問老師類接口
void visit(Teacher person);
// 提供訪問學生類接口
void visit(Student person);
}
3.兩個具體訪問者類:
科研獎資格判斷類:
// 科研獎資格判斷類(具體訪問者類角色)
public class ResearchAward implements Award {
@Override
public void visit(Teacher person) {
int paperNums = person.getPaperNums();
if (paperNums > 10) {
System.out.println(person.getName() + "老師發(fā)表論文數為:" + paperNums + ",擁有評選科研獎資格");
}
}
@Override
public void visit(Student person) {
int paperNums = person.getPaperNums();
if (paperNums > 2) {
System.out.println(person.getName() + "同學發(fā)表論文數為:" + paperNums + "朴爬,擁有評選科研獎資格");
}
}
}
成績優(yōu)秀獎資格判斷類:
// 成績優(yōu)秀獎資格判斷類(具體訪問者類)
public class ExcellentAward implements Award {
@Override
public void visit(Teacher person) {
if (person.getFeedbackScore() >= 90) {
System.out.println(person.getName() + "老師發(fā)表教學反饋分為:" + person.getFeedbackScore() + ",擁有評選成績優(yōu)秀獎資格");
}
}
@Override
public void visit(Student person) {
if (person.getAverageScore() >= 90) {
System.out.println(person.getName() + "同學平均成績?yōu)椋? + person.getAverageScore() + "橡淆,擁有評選成績優(yōu)秀獎資格");
}
}
}
4.對象結構類:
// 獎勵審批系統(tǒng)(對象結構類角色)
public class AwardCheckSystem {
// 存放元素的容器
private List<Person> personList = new ArrayList<>();
// 添加元素方法
public void addPerson(Person person) {
personList.add(person);
}
// 系統(tǒ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("擁有評選科研獎資格的人有:");
awardCheckSystem.awardCheck(new ResearchAward());
System.out.println("----------------------------------------------");
System.out.println("擁有評選成績優(yōu)秀獎資格的人有:");
awardCheckSystem.awardCheck(new ExcellentAward());
}
}
擁有評選科研獎資格的人有:
李四老師發(fā)表論文數為:11召噩,擁有評選科研獎資格
趙六同學發(fā)表論文數為:3,擁有評選科研獎資格
----------------------------------------------
擁有評選成績優(yōu)秀獎資格的人有:
張三老師發(fā)表教學反饋分為:91逸爵,擁有評選成績優(yōu)秀獎資格
王五同學平均成績?yōu)椋?2具滴,擁有評選成績優(yōu)秀獎資格
經過改造之后輸出結果和上面的一摸一樣,但獎勵審批系統(tǒng)判斷獎項評選資格的核心代碼變得非常簡潔师倔。同時如果要有其他的獎項資格判斷构韵,只需要增加一個新的具體訪問者類并在新的獎項資格判斷類中添加具體的判斷邏輯就可以了,大大提高了系統(tǒng)的可擴展性。
訪問者模式也有一個很是明顯的問題疲恢,它在添加新的訪問者的時候是很容易的凶朗,但在添加新的元素時較為麻煩。在這個獎項審批系統(tǒng)案例里面因為需求就是判斷老師和學生是否有評獎資格显拳,涉及到的元素只有老師和學生棚愤,應該也不會出現變化,但是還有可能評選其他獎項的資格杂数,所以這里用訪問者模式是很合適的宛畦。
模式應用
訪問者模式在合適的場景下使用之后,會使代碼變得更加靈活易于擴展揍移。下面通過介紹它在 Spring 中的具體應用次和,讓我們對模式的應用更加深刻。
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.簡單的定義一個實體類:
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
類實例那伐,因此配置一個person.properties
文件:
person.name=張三
person.age=18
5.然后簡單的使用:
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.使用結果:
Person{name='張三', age=18}
這里只是簡單的輸出了初始化對象的信息踏施。重點是想要說明的這里使用占位符${}
的方式將配置文件person.properties
的信息設置到對象字段里面,在 Spring 中是通過PropertySourcesPlaceholderConfigurer
類中的processProperties()
方法中完成的喧锦,而方法內部又調用了PlaceholderConfigurerSupport
類中的doProcessProperties()
方法读规,在doProcessProperties
內部就使用到了BeanDefinitionVisitor
類,這個類就代表了訪問者類燃少。通過追蹤源碼可以下面的關鍵代碼:
protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
StringValueResolver valueResolver) {
// 通過 BeanDefinitionVisitor 類的 visitBeanDefinition() 方法來實現訪問者模式的核心思想
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ā)現BeanDefinition
是一個接口,也就是訪問者模式中的抽象元素角色阵具,而它的子類有RootBeanDefinition
碍遍、ChildBeanDefinition
和GenericBeanDefinition
等等,這些可以理解為具體的元素角色阳液。需要注意的是怕敬,這里的BeanDefinition
明顯是一個實現類,也就是說在 Spring 中并沒有抽象出抽象訪問者來對具體訪問者類進行擴展帘皿,但是訪問者模式的思想在上面幾個類之間的運用得到了充分的體現东跪。
總結
主要優(yōu)點
- 增加新的訪問操作很方便。使用訪問者模式鹰溜,增加新的訪問操作就意味著增加一個新的具體訪問者類虽填,實現簡單,無須修改源代碼曹动,符合“開閉原則”斋日。
- 將有關元素對象的訪問行為集中到一個訪問者對象中,而不是分散在一個個的元素類中墓陈。類的職責更加清晰恶守,有利于對象結構中元素對象的復用第献,相同的對象結構可以供多個不同的訪問者訪問。
- 讓用戶能夠在不修改現有元素類層次結構的情況下兔港,定義作用于該層次結構的操作庸毫。
主要缺點
- 增加新的元素類很困難。在訪問者模式中押框,每增加一個新的元素類都意味著要在抽象訪問者角色中增加一個新的抽象操作岔绸,并在每一個具體訪問者類中增加相應的具體操作,這違背了“開閉原則”的要求橡伞。
- 破壞封裝盒揉。訪問者模式要求訪問者對象訪問并調用每一個元素對象的操作,這意味著元素對象有時候必須暴露一些自己的內部操作和內部狀態(tài)兑徘,否則無法供訪問者訪問刚盈。
適用場景
- 一個對象結構包含多個類型的對象,希望對這些對象實施一些依賴其具體類型的操作挂脑。在訪問者中針對每一種具體的類型都提供了一個訪問操作藕漱,不同類型的對象可以有不同的訪問操作。
- 需要對一個對象結構中的對象進行很多不同的并且不相關的操作崭闲,而需要避免讓這些操作“污染”這些對象的類肋联,也不希望在增加新操作時修改這些類。訪問者模式使得我們可以將相關的訪問操作集中起來定義在訪問者類中刁俭,對象結構可以被多個不同的訪問者類所使用橄仍,將對象本身與對象的訪問操作分離。
- 對象結構中對象對應的類很少改變牍戚,但經常需要在此對象結構上定義新的操作侮繁。
參考資料
- 大話設計模式
- 設計模式Java版本-劉偉
- 設計模式深入淺出--24.訪問者模式簡單實例及其在JDK、Spring中的應用
本篇文章github代碼地址:https://github.com/Phoegel/design-pattern/tree/main/visitor
轉載請說明出處如孝,本篇博客地址:http://www.reibang.com/p/875a0a822fd1