访问者模式

访问者模式

案例

张三所在公司欲为某高校开发一套奖励审批系统,该系统可以实现教师奖励和学生奖励的审批(Award Check),如果教师发表论文数超过10篇或者学生论文超过2篇可以评选科研奖,如果教师教学反馈分大于等于90分或者学生平均成绩大于等于90分可以评选成绩优秀奖。该系统主要用于判断候选人集合中的教师或学生是否符合某种获奖要求。张三想了想就开始动手写起来了。

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.奖励审批系统关键代码:

// 奖励审批系统
public class AwardCheckSystem {
    // 存放元素的容器
    private List<Person> personList = new ArrayList<>();

    // 添加元素方法
    public void addPerson(Person person) {
        personList.add(person);
    }

    // 系统判断评选资格核心代码
    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() + "老师发表论文数为:" + paperNums + ",拥有评选科研奖资格");
                } else if (person instanceof Student && paperNums > 2) {
                    System.out.println(person.getName() + "同学发表论文数为:" + paperNums + ",拥有评选科研奖资格");
                }
            }
        } else if (prize.equals("excellent")) {
            for (Person person : personList) {
                if (person instanceof Teacher && ((Teacher) person).getFeedbackScore() >= 90) {
                    System.out.println(person.getName() + "老师发表教学反馈分为:" + ((Teacher) person).getFeedbackScore() + ",拥有评选成绩优秀奖资格");
                } else if (person instanceof Student && ((Student) person).getAverageScore() >= 90) {
                    System.out.println(person.getName() + "同学平均成绩为:" + ((Student) person).getAverageScore() + ",拥有评选成绩优秀奖资格");
                }
            }
        }
    }
}

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("拥有评选成绩优秀奖资格的人有:");
        awardCheckSystem.awardCheck("excellent");
    }
}

5.使用结果:

拥有评选科研奖资格的人有:
李四老师发表论文数为:11,拥有评选科研奖资格
赵六同学发表论文数为:3,拥有评选科研奖资格
----------------------------------------------
拥有评选成绩优秀奖资格的人有:
张三老师发表教学反馈分为:91,拥有评选成绩优秀奖资格
王五同学平均成绩为:92,拥有评选成绩优秀奖资格

张三很快就写出了奖励审批系统中最核心的代码,但是他觉得在awardCheck()方法中通过奖项名称和人员类型判断是否有资格评选奖项的代码看上去很是复杂,他想要改进一下。刚好在设计模式中对于这种集合对象中存在多种不同元素,同时对于这些不同元素不同的处理者会有不同的处理方式的情况可以使用访问者模式对其进行改进。

模式介绍

访问者模式(Visitor Pattern):提供一个作用于某对象结构中的各元素的操作表示,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一种对象行为型模式。

角色构成

  • Vistor(抽象访问者):抽象访问者为对象结构中每一个具体元素类ConcreteElement声明一个访问操作,从这个操作的名称或参数类型可以清楚知道需要访问的具体元素的类型,具体访问者需要实现这些操作方法,定义对这些元素的访问操作。
  • ConcreteVisitor(具体访问者):具体访问者实现了每个由抽象访问者声明的操作,每一个操作用于访问对象结构中一种类型的元素。
  • Element(抽象元素):抽象元素一般是抽象类或者接口,它定义一个accept()方法,该方法通常以一个抽象访问者作为参数。【稍后将介绍为什么要这样设计。】
  • ConcreteElement(具体元素):具体元素实现了accept()方法,在accept()方法中调用访问者的访问方法以便完成对一个元素的操作。
  • ObjectStructure(对象结构):对象结构是一个元素的集合,它用于存放元素对象,并且提供了遍历其内部元素的方法。它可以结合组合模式来实现,也可以是一个简单的集合对象,如一个List对象或一个Set对象。

UML 类图

访问者模式 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() + "老师发表论文数为:" + paperNums + ",拥有评选科研奖资格");
        }
    }

    @Override
    public void visit(Student person) {
        int paperNums = person.getPaperNums();
        if (paperNums > 2) {
            System.out.println(person.getName() + "同学发表论文数为:" + paperNums + ",拥有评选科研奖资格");
        }
    }
}

成绩优秀奖资格判断类:

// 成绩优秀奖资格判断类(具体访问者类)
public class ExcellentAward implements Award {
    @Override
    public void visit(Teacher person) {
        if (person.getFeedbackScore() >= 90) {
            System.out.println(person.getName() + "老师发表教学反馈分为:" + person.getFeedbackScore() + ",拥有评选成绩优秀奖资格");
        }
    }

    @Override
    public void visit(Student person) {
        if (person.getAverageScore() >= 90) {
            System.out.println(person.getName() + "同学平均成绩为:" + person.getAverageScore() + ",拥有评选成绩优秀奖资格");
        }
    }
}

4.对象结构类:

// 奖励审批系统(对象结构类角色)
public class AwardCheckSystem {
    // 存放元素的容器
    private List<Person> personList = new ArrayList<>();

    // 添加元素方法
    public void addPerson(Person person) {
        personList.add(person);
    }

    // 系统判断评选资格核心代码
    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("拥有评选成绩优秀奖资格的人有:");
        awardCheckSystem.awardCheck(new ExcellentAward());
    }
}
拥有评选科研奖资格的人有:
李四老师发表论文数为:11,拥有评选科研奖资格
赵六同学发表论文数为:3,拥有评选科研奖资格
----------------------------------------------
拥有评选成绩优秀奖资格的人有:
张三老师发表教学反馈分为:91,拥有评选成绩优秀奖资格
王五同学平均成绩为:92,拥有评选成绩优秀奖资格

经过改造之后输出结果和上面的一摸一样,但奖励审批系统判断奖项评选资格的核心代码变得非常简洁。同时如果要有其他的奖项资格判断,只需要增加一个新的具体访问者类并在新的奖项资格判断类中添加具体的判断逻辑就可以了,大大提高了系统的可扩展性。

访问者模式也有一个很是明显的问题,它在添加新的访问者的时候是很容易的,但在添加新的元素时较为麻烦。在这个奖项审批系统案例里面因为需求就是判断老师和学生是否有评奖资格,涉及到的元素只有老师和学生,应该也不会出现变化,但是还有可能评选其他奖项的资格,所以这里用访问者模式是很合适的。

模式应用

访问者模式在合适的场景下使用之后,会使代码变得更加灵活易于扩展。下面通过介绍它在 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()方法的方法签名,可以发现BeanDefinition是一个接口,也就是访问者模式中的抽象元素角色,而它的子类有RootBeanDefinitionChildBeanDefinitionGenericBeanDefinition等等,这些可以理解为具体的元素角色。需要注意的是,这里的BeanDefinition明显是一个实现类,也就是说在 Spring 中并没有抽象出抽象访问者来对具体访问者类进行扩展,但是访问者模式的思想在上面几个类之间的运用得到了充分的体现。

总结

主要优点

  • 增加新的访问操作很方便。使用访问者模式,增加新的访问操作就意味着增加一个新的具体访问者类,实现简单,无须修改源代码,符合“开闭原则”。
  • 将有关元素对象的访问行为集中到一个访问者对象中,而不是分散在一个个的元素类中。类的职责更加清晰,有利于对象结构中元素对象的复用,相同的对象结构可以供多个不同的访问者访问。
  • 让用户能够在不修改现有元素类层次结构的情况下,定义作用于该层次结构的操作。

主要缺点

  • 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”的要求。
  • 破坏封装。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。

适用场景

  • 一个对象结构包含多个类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作。
  • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。访问者模式使得我们可以将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用,将对象本身与对象的访问操作分离。
  • 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。

参考资料

本篇文章github代码地址:https://github.com/Phoegel/design-pattern/tree/main/visitor
转载请说明出处,本篇博客地址:https://www.jianshu.com/p/875a0a822fd1

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345