十四、观察者模式与访问者模式详解

21.观察者模式

21.1.课程目标

1、 掌握观察者模式和访问者模式的应用场景。

2、 掌握观察者模式在具体业务场景中的应用。

3、 了解访问者模式的双分派。

4、 观察者模式和访问者模式的优、缺点。

21.2.内容定位

1、 有 Swing开发经验的人群更容易理解观察者模式。

2、 访问者模式被称为最复杂的设计模式。

21.3.观察者模式

观 察 者 模 式 ( Observer Pattern ) , 又叫发布-订阅( Publish/Subscribe ) 模式、模型-视图 ( Model/View ) 模式、源-监听器(Source/Listener) 模式或从属者( Dependents ) 模式。定义一种一对多的依赖关系,一个主题对象可被多个观察者对象同时监听,使得每当主题对象状态变化时,所
有依赖于它的对象都会得到通知并被自动更新。属于行为型模式。

原文:Defines a one-to-many dependency relationship between objects so that each time an object’ s state changes, its dependent objects are notified and automatically updated.

观察者模式的核心是将观察者与被观察者解耦,以类似于消息/广播发送的机制联动两者,使被观察
者的变动能通知到感兴趣的观察者们,从而做出相应的响应。

21.4.应用场景

观察者模式在现实生活应用也非常广泛,比如:起床闹钟设置、APP角标通知、GPer生态圈消息通知、邮件通知、广播通知、桌面程序的事件响应等(如下图)。
APP角标通知

image-20200323210257533

在软件系统中,当系统一方行为依赖于另一方行为的变动时,可使用观察者模式松耦合联动双方,
使得一方的变动可以通知到感兴趣的另一方对象,从而让另一方对象对此做出响应。观察者模式适用于
以下几种应用场景:

1、当一个抽象模型包含两个方面内容,其中一个方面依赖于另一个方面;

2、其他一个或多个对象的变化依赖于另一个对象的变化;

3、实现类似广播机制的功能,无需知道具体收听者,只需分发广播,系统中感兴趣的对象会自动接收该广播;

4、多层级嵌套使用,形成一种链式触发机制,使得事件具备跨域(跨越两种观察者类型)通知。

下面来看下观察者模式的通用UML类 图 :

image-20200324234145502

从 UML类图中,我们可以看到,观察者模式主要包含三种角色:

抽象主题(Subject) :指被观察的对象(Observable ) 。该角色是一个抽象类或接口,定义了增
加、删除、通知观察者对象的方法;

具体主题(ConcreteSubject) :具体被观察者,当其内状态变化时,会通知已注册的观察者;

抽象观察者(Observer) :定义了响应通知的更新方法;

具体观察者(ConcreteObserver) :在得到状态更新时,会自动做出响应。

21.5.观察者模式在业务场景中的应用

当小伙伴们在GPer生态圈中提问的时候,如果有设置指定老师回答,对应的老师就会收到邮件通 知 ,这就是观察者模式的一种应用场景。我们有些小伙伴可能会想到MQ ,异步队列等,其实JDK本身
就提供这样的APIO 我们用代码来还原一下这样一个应用场景,创建GPer类:

/**
 * JDK提供的一种观察者的实现方式,被观察者
 */
public class GPer extends Observable {
    private String name = "GPer生态圈";
    private static final GPer gper = new GPer();

    private GPer() {}

    public static GPer getInstance(){
        return gper;
    }

    public String getName() {
        return name;
    }

    public void publishQuestion(Question question){
        System.out.println(question.getUserName() + "在" + this.name + "上提交了一个问题。");
        setChanged();
        notifyObservers(question);
    }
}

创建问题Question类:

public class Question {
    private String userName;
    private String content;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

创建老师Teacher类:

public class Teacher implements Observer {

    private String name;

    public Teacher(String name) {
        this.name = name;
    }

    public void update(Observable o, Object arg) {
        GPer gper = (GPer)o;
        Question question = (Question)arg;
        System.out.println("======================");
        System.out.println(name + "老师,你好!\n" +
                        "您收到了一个来自" + gper.getName() + "的提问,希望您解答。问题内容如下:\n" +
                        question.getContent() + "\n" +
                        "提问者:" + question.getUserName());
    }
}

客户端测试代码:

public class Test {
    public static void main(String[] args) {
        GPer gper = GPer.getInstance();
        Teacher tom = new Teacher("Tom");
        Teacher jerry = new Teacher("Jerry");
        gper.addObserver(tom);
        gper.addObserver(jerry);
        //用户行为
        Question question = new Question();
        question.setUserName("张三");
        question.setContent("观察者模式适用于哪些场景?");
        gper.publishQuestion(question);
    }
}

运行结果:

image-20200324235339975

21.6.基于Guava API轻松落地观察者模式

给大家推荐一个实现观察者模式非常好用的框架。API使用也非常简单,举个例子,先引入maven
依赖包:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>18.0</version>
</dependency>

创建侦听事件 :

public class PojoEvent {

    @Subscribe
    public void observer(Pojo pojo){
        System.out.println("执行PojoEvent方法,传参为:" + pojo);
    }
}
public class VoEvent {

    @Subscribe
    public void observer(Vo arg){
    	System.out.println("执行VoEvent方法,传参为:" + arg);
    }
}

客户端测试代码:

public class Test {
    public static void main(String[] args) {
        EventBus eventBus = new EventBus();
        PojoEvent guavaEvent = new PojoEvent();
        VoEvent voEvent = new VoEvent();
        eventBus.register(guavaEvent);
        eventBus.register(voEvent);
        eventBus.post(new Pojo("Tom"));
    }
}

运行结果:

image-20200325004201180

21.7.使用观察者模式设计鼠标事件响应API

下面再来设计一个业务场景,帮助小伙伴更好的理解观察者模式。JDK源码中,观察者模式也应用
非常多。例如java.awt.Event就是观察者模式的一种,只不过Java很少被用来写桌面程序。我们自己 用代码来实现一下,以帮助小伙伴们更深刻地了解观察者模式的实现原理。首先,创建EventListener
接口 :

/**
 * 观察者抽象
 */
public interface EventListener {
}

创建Event类:

@Data
public class Event {
    //事件源,动作是由谁发出的
    private Object source;
    //事件触发,要通知谁(观察者)
    private EventListener target;
    //观察者给的回应
    private Method callback;
    //事件的名称
    private String trigger;
    //事件的触发事件
    private long time;

    public Event(EventListener target, Method callback) {
        this.target = target;
        this.callback = callback;
    }
}

创建 EventContext 类 :

/**
 * 被观察者的抽象
 */
public class EventContext {
    protected Map<String,Event> events = new HashMap<String,Event>();

    public void addLisenter(String eventType, EventListener target, Method callback){
        events.put(eventType,new Event(target,callback));
    }

    public void addLisenter(String eventType, EventListener target){
        try {
            this.addLisenter(eventType, target, target.getClass().getMethod("on" + toUpperFirstCase(eventType), Event.class));
        }catch (NoSuchMethodException e){
            return;
        }
    }

    private String toUpperFirstCase(String eventType) {
        char [] chars = eventType.toCharArray();
        chars[0] -= 32;
        return String.valueOf(chars);
    }

    private void trigger(Event event){
        event.setSource(this);
        event.setTime(System.currentTimeMillis());

        try {
            if (event.getCallback() != null) {
                //用反射调用回调函数
                event.getCallback().invoke(event.getTarget(), event);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    protected void trigger(String trigger){
        if(!this.events.containsKey(trigger)){return;}
        trigger(this.events.get(trigger).setTrigger(trigger));
    }
}

创建 MouseEventType 接口 :

public interface MouseEventType {
    String ON_CLICK = "click";

    String ON_MOVE = "move";
}

创建Mouse类 :

/**
 * 具体的被观察者
 */
public class Mouse extends EventContext {
    public void click(){
        System.out.println("调用单击方法");
        this.trigger(MouseEventType.ON_CLICK);
    }

    public void move(){
        System.out.println("调用移动方法");
        this.trigger(MouseEventType.ON_MOVE);
    }
}

创建回调方法MouseEventLisenter类 :

/**
 * 观察者
 */
public class MouseEventLisenter implements EventListener {

    public void onClick(Event e){
        System.out.println("==========触发鼠标单击事件========\n" + e);
    }

    public void onMove(Event e){

        System.out.println("==========触发鼠标移动事件========\n" + e);
    }
}

客户端测试代码:

public class Test {
    public static void main(String[] args) {
        MouseEventLisenter lisenter = new MouseEventLisenter();
        Mouse mouse = new Mouse();
        mouse.addLisenter(MouseEventType.ON_CLICK,lisenter);
        mouse.addLisenter(MouseEventType.ON_MOVE,lisenter);
        mouse.click();
        mouse.move();
    }
}

运行结果:

image-20200325004642820

21.8.观察者模式在源码中的应用

Spring 中 的 ContextLoaderListener 实 现 了 ServletContextListener 接 口 ServletContextListener 接口又继承了 EventListener,在 JDK 中 EventListener 有非常广泛的应用。

我们可以看一下源代码,ContextLoaderListener:

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    
   public ContextLoaderListener() {
   }

   public ContextLoaderListener(WebApplicationContext context) {
      super(context);
   }

   @Override
   public void contextInitialized(ServletContextEvent event) {
      initWebApplicationContext(event.getServletContext());
   }

   @Override
   public void contextDestroyed(ServletContextEvent event) {
      closeWebApplicationContext(event.getServletContext());
      ContextCleanupListener.cleanupAttributes(event.getServletContext());
   }
}

ServletContextListener 接口源码如下:

public interface ServletContextListener extends EventListener {
    void contextInitialized(ServletContextEvent var1);

    void contextDestroyed(ServletContextEvent var1);
}

EventListener接口源码如下:

public interface EventListener {
}

21.7.观察者模式的优缺点

优点:

1、观察者和被观察者是松耦合(抽象耦合)的 ,符合依赖倒置原则;

2、分离了表示层(观察者)和数据逻辑层(被观察者 儿 并且建立了一套触发机制,使得数据的变
化可以响应到多个表示层上;

3、实现了一对多的通讯机制,支持事件注册机制,支持兴趣分发机制,当被观察者触发事件时,只
有感兴趣的观察者可以接收到通知。

缺点:

1、 如果观察者数量过多,则事件通知会耗时较长;

2、 事件通知呈线性关系,如果其中一个观察者处理事件卡壳,会影响后续的观察者接收该事件;

3、 如果观察者和被观察者之间存在循环依赖,则可能造成两者之间的循环调用,导致系统崩溃。

21.8.思维导图

image-20200325134241322

22.访问者模式

22.1.定义

访问者模式(Visitor Pattern )是一种将数据结构与数据操作分离的设计模式。是指封装一 些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些
元素的新的操作。属于行为型模式。

原文:Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.

访问者模式被称为最复杂的设计模式,并且使用频率不高,设计模式的作者也评价为:大多
情况下,你不需要使用访问者模式,但是一旦需要使用它时,那就真的需要使用了。访问者模
式的基1=1 本思想是,针对系统中拥有固定类型数的对象结构(元素),在其内提供一个accept()方法用来接受访问者对象的访问。不同的访问者对同一元素的访问内容不同,使得相同的元素
集合可以产生不同的数据结果。accept()方法可以接收不同的访问者对象,然后在内部将自己(元 素 )转发到接收到的访问者对象的visit()方法内。访问者内部对应类型的visit()方法就会得到回 调执行,对元素进行操作。也就是通过两次动态分发(第一次是对访问者的分发accept()方法, 第二次是对元素的分发visit()方法),才最终将一个具体的元素传递到一个具体的访问者。如此 —来 ,就解耦了数据结构与操作,且数据操作不会改变元素状态。

访问者模式的核心是,解耦数据结构与数据操作,使得对元素的操作具备优秀的扩展性。可
以通过扩展不同的数据操作类型(访问者)实现对相同元素集的不同的操作。

22.2.应用场景

访问者模式在生活场景中也是非常当多的,例如每年年底的KPI考核,KPI考核标准是相对
稳定的,但是参与KPI考核的员工可能每年都会发生变化,那么员工就是访问者。我们平时去
食堂或者餐厅吃饭,餐厅的菜单和就餐方式是相对稳定的,但是去餐厅就餐的人员是每天都在
发生变化的,因此就餐人员就是访问者。

image-20200325005056359

当系统中存在类型数目稳定( 定 )的一类数据结构时,可以通过访问者模式方便地实现对
该类型所有数据结构的不同操作,而又不会数据产生任何副作用(脏数据)。

简言之,就是对集合中的不同类型数据(类型数量稳定)进行多种操作,则使用访问者模式。
下面总结一下访问者模式的适用场景:

1、 数据结构稳定,作用于数据结构的操作经常变化的场景;

2、 需要数据结构与数据操作分离的场景;

3、 需要对不同数据类型(元素)进行操作,而不使用分支判断具体类型的场景。

首先来看下访问者模式的通用UML类图:

image-20200325005710474

从 UML类图中,我们可以看到,访问者模式主要包含五种角色:

抽象访问者(Visitor):接口或抽象类,该类地冠以了对每一个具体元素(Element )的访 问行为visit()方法,其参数就是具体的元素(Element)对象。理论上来说,Visitor的方法个 数与元素(Element )个数是相等的。如果元素(Element )个数经常变动,会导致Visitor的 方法也要进行变动,此时,该情形并不适用访问者模式;

具体访问者(ConcreteVisitor):实现对具体元素的操作;

抽象元素(Element ):接口或抽象类,定义了一个接受访问者访问的方法accept() , 表示 所有元素类型都支持被访问者访问;

具体元素(Concrete Element ):具体元素类型,提供接受访问者的具体实现。通常的实
现 都 为 :visitor.visit(this);

结构对象(ObjectStruture ):该类内部维护了元素集合,并提供方法接受访问者对该集合所有元素进行操作。

22.3.利用访问者模式实现KPI考核的场景

每到年底,管理层就要开始评定员工一年的工作绩效,员工分为工程师和经理;管理层有
CEO和 CTO。那么CTO关注工程师的代码量、经理的新产品数量;CEO关注的是工程师的KPI
和经理的KPI以及新产品数量。

由于CEO和 CTO对于不同员工的关注点是不一样的,这就需要对不同员工类型进行不同的
处理。访问者模式此时可以派上用场了。

// 员工基类
public abstract class Employee {
    public String name;
    public int kpi;  //员工KPI

    public Employee(String name) {
        this.name = name;
        kpi = new Random().nextInt(10);
    }

    //接收访问者的访问
    public abstract void accept(IVisitor visitor);
}

Employee类定义了员工基本信息及一个acceptQ方法,acceptQ方法表示接受访问者的访 问 ,由具体的子类来实现。访问者是个接口,传入不同的实现类,可访问不同的数据。下面看
看工程师Engineer类的代码:

// 工程师类
public class Engineer extends Employee {
    public Engineer(String name) {
        super(name);
    }

    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }

    //考核指标是每年的代码量
    public int getCodeLines(){
        return new Random().nextInt(10* 10000);
    }
}

经理Manager类的代码:

public class Manager extends Employee {
    public Manager(String name) {
        super(name);
    }

    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }

    //考核的是每年新产品研发数量
    public int getProducts(){
        return new Random().nextInt(10);
    }
}

工程师是考核的是代码数量,经理考核的是产品数量,二者的职责不一样。也正是因为有这
样的差异性,才使得访问模式能够在这个场景下发挥作用。Employee. Engineer. Manager
这 3个类型就相当于数据结构,这些类型相对稳定,不会发生变化。

然后将这些员工添加到一个业务报表类中,公司高层可以通过该报表类的showReport()方 法查看所有员工的业绩,具体代码如下:

public class BusinessReport {
    private List<Employee> employees = new LinkedList<Employee>();

    public BusinessReport() {
        employees.add(new Manager("产品经理A"));
        employees.add(new Engineer("程序员A"));
        employees.add(new Engineer("程序员B"));
        employees.add(new Engineer("程序员C"));
        employees.add(new Manager("产品经理B"));
        employees.add(new Engineer("程序员D"));
    }

    public void showReport(IVisitor visitor){
        for (Employee employee : employees) {
            employee.accept(visitor);
        }
    }
}

下面看看访问者类型的定义,访问者声明了两个visit()方法,分别对工程师和经理访问,具
体代码如下:

public interface IVisitor {
    void visit(Engineer engineer);

    void visit(Manager manager);
}

首先定义一个IVisitor接 口 ,该接口有两个visit()方 法 ,参数分别是Engineer. Manager ,
也就是说对于Engineer和 Manager的访问会调用两个不同的方法,以此达到差异化处理的目 的。这两个访问者具体的实现类为CEOVisitor类和CTOVisitor类 ,代码如下:

public class CEOVistitor implements IVisitor {
    public void visit(Engineer engineer) {
        System.out.println("工程师" +  engineer.name + ",KIP:" + engineer.kpi);
    }

    public void visit(Manager manager) {
        System.out.println("经理:" +  manager.name + ",KPI:" + manager.kpi + ",产品数量:" + manager.getProducts());
    }
}

这就导致了 if-else逻辑的嵌套以及类型的强制转换,难以扩展和维护,当类型较多时,这 个 ReportUtil就会很复杂。而使用访问者模式,通过同一个函数对不同对元素类型进行相应对 处理,使结构更加清晰、灵活性更高。

再添加一个CTO的访问者类:

public class CTOVistitor implements IVisitor {
    public void visit(Engineer engineer) {
        System.out.println("工程师" +  engineer.name + ",代码行数:" + engineer.getCodeLines());
    }

    public void visit(Manager manager) {
        System.out.println("经理:" +  manager.name + ",产品数量:" + manager.getProducts());
    }
}

重载的visitO方法会对元素进行不同的操作,而通过注入不同的访问者又可以替换掉访问者 的具体实现,使得对元素的操作变得更灵活,可扩展性更高,同时也消除了类型转换.if-else等
“丑陋”的代码。

下面是客户端代码:

public class Test {
    public static void main(String[] args) {
        BusinessReport report = new BusinessReport();
        System.out.println("==========CEO看报表===============");
        report.showReport(new CEOVistitor());
        System.out.println("==========CTO看报表===============");
        report.showReport(new CTOVistitor());
    }
}

运行结果如下:

image-20200325010921337

在上述案例中, Employee扮演了 Element角 色 ,而 Engineer和 Manager都是 ConcreteElement ; CEOVisitor 和 CTOVisitor 都是具体的 Visitor 对 象 ; 而 BusinessReport
就是 Objectstructure。

访问者模式最大的优点就是增加访问者非常容易,我们从代码中可以看到,如果要增加一个
访问者,只要新实现一个访问者接口的类,从而达到数据对象与数据操作相分离的效果。如果
不实用访问者模式,而又不想对不同的元素进行不同的操作,那么必定需要使用if-else和类型 转换,这使得代码难以升级维护。

我们要根据具体情况来评估是否适合使用访问者模式,例如,我们的对象结构是否足够稳定,
是否需要经常定义新的操作,使用访问者模式是否能优化我们的代码,而不是使我们的代码变
得更复杂。

22.4.从静态分派到动态分派

变量被声明时的类型叫做变量的静态类型(Static Type), 有些人又把静态类型叫做明显类型
(Apparent Type); 而变量所引用的对象的真实类型又叫做变量的实际类型(Actual Type)o 比
如 :

List list = null; 
list = new ArrayList();

声明了一个变 量list ,它的静态类型(也叫明显类型)是 List ,而它的实际类型是ArrayList。 根据对象的类型而对方法进行的选择,就是分派(Dispatch)。分派又分为两种,即静态分派和动
态分派。

1、静态分派

静态分派(Static Dispatch)就是按照变量的静态类型进行分派,从而确定方法的执行版本, 静态分派在编译时期就可以确定方法的版本。而静态分派最典型的应用就是方法重载’来看下面
这段代码。

public class Main {
    public void test(String string){
        System.out.println("string" + string);
    }
    public void test(Integer integer){
        System.out.println("integer" + integer);
    }

    public static void main(String[] args) {
        String string = "1";
        Integer integer = 1;
        Main main = new Main();
        main.test(integer);
        main.test(string);
    }
}

在静态分派判断的时候,我们根据多个判断依据(即参数类型和个数)判断出了方法的版本,
那么这个就是多分派的概念,因为我们有一个以上的考量标准。所以Java是静态多分派的语言。

2、动态分派

对于动态分派,与静态相反,它不是在编译期确定的方法版本,而是在运行时才能确定。而
动态分派最典型的应用就是多态的特性。举个例子,来看下面的这段代码。

public interface Person {
    void test();
}

public class Man implements Person {
    public void test() {
        System.out.println("男人");
    }
}

public class WoMan implements Person {
    public void test() {
        System.out.println("女人");
    }
}

public class Main {
    public static void main(String[] args) {
        Person man = new Man();
        Person woman = new WoMan();

        man.test();
        woman.test();
    }
}

这段程序输出结果为依次打印男人和女人,然而这里的test。方法版本,就无法根据Man 和 Woman的静态类型去判断了,他们的静态类型都是Person接口,根本无从判断。

显然,产生的输出结果,就是因为test。方法的版本是在运行时判断的,这就是动态分派。

动态分派判断的方法是在运行时获取到Man和 Woman的实际引用类型,再确定方法的版 本 ,而由于此时判断的依据只是实际引用类型,只有一个判断依据,所以这就是单分派的概念,
这时我们的考量标准只有一个,即变量的实际引用类型。相应的,这说明Java是动态单分派的
语言。

22.5.访问者模式中的伪动态双分派

通过前面分析,我们知道Java是静态多分派、动态单分派的语言。Java底层不支持动态的双分派。但是通过使用设计模式,也可以在Java语言里实现伪动态双分派。在访问者模式中使 用的就是伪动态双分派。所谓动态双分派就是在运行时依据两个实际类型去判断一个方法的运
行行为,而访问者模式实现的手段是进行了两次动态单分派来达到这个效果。

还是回到前面的KPI考核业务场景当中, BusinessReport类中的showReport()方 法 :

public void showReport(IVisitor visitor) { 
    for (Employee employee : employees) { 
        employee.accept(visitor);
	}
}

这里就是依据Employee和 IVisitor两个实际类型决定了 showReport()方法的执行结果,
从而决定了 accept()方法的动作。

分析accept()方法的调用过程

  1. 当调用accept()方法时,根 据 Employee的实际类型决定是调用Engineer还 是 Manager
    的 accept()方法。
  2. 这时accept()方法的版本已经确定,假如是Engineer , 它的accept()方法是调用下面这
    行代码。
public void accept(IVisitor visitor) { 
    visitor .visit (this);
}

此时的th is是 Engineer类 型 ,所以对应的是IVisitor接口的visit(Engineer engineer)方
法 ,此时需要再根据访问者的实际类型确定Visit()方法的版本,如此一来,就完成了动态双分派
的过程。

以上的过程就是通过两次动态双分派,第一次对accept方法进行动态分派,第二次对访问 者的visit方法进行动态分派,从而达到了根据两个实际类型确定一个方法的行为的效果。

而原本我们的做法,通常是传入一个接口,直接使用该接口的方法,此为动态单分派,就像策略模式一样。在这里,showReport()方法传入的访问者接口并不是直接调用自己的visit()方 法 ,而是通过Employee的实际类型先动态分派一次,然后在分派后确定的方法版本里再进行
自己的动态分派。

注意:这里确定accept (IVisitor visitor)方法是静态分派决定的,所以这个并不在此次动态双分派的范畴内,而且 静态分派是在编译期就完成的,所以accept (I Visitor vi si tor)方法的静态分派与访问者模式的动态双分派并没有任 何关系。动态双分派说到底还是动态分派,是在运行时发生的,它与静态分派有着本质上的区别,不可以说一次动态
分派加一次静态分派就是动态双分派,而且访问者模式的双分派本身也是另有所指。

而 this的类型不是动态确定的,你写在哪个类当中,它的静态类型就是哪个类,这是在编译 期就确定的,不确定的是它的实际类型,请小伙伴也要区分开来。

22.6.访问者模式在源码中的应用

首先来看JDK的 NIO模块下的Filevisitor,它接口提供了递归遍历文件树的支持。这个接口 上的方法表示了遍历过程中的关键过程,允许你在文件被访问、目录将被访问、目录已被访问、
发生错误等等过程上进行控制;换句话说,这个接口在文件被访问前、访问中和访问后,以及
产生错误的时候都有相应的钩子程序进行处理。

调 用 FileVisitor中的方法,会返回访问结果FileVisitResult对 象 值 ,用于决定当前操作完
成后接下来该如何处理。FileVisitResult的标准返回值存放到FileVisitResult枚举类型中:

FileVisitResult.CONTINUE : 这个访问结果表示当前的遍历过程将会继续。

FileVisitResult.SKIP SIBLINGS :这个访问结果表示当前的遍历过程将会继续,但是要
忽略当前文件/目录的兄弟节点。

FileVisitResult.SKIP SUBTREE : 这个访问结果表示当前的遍历过程将会继续,但是要
忽略当前目录下的所有节点。

FileVisitResult.TERMINATE : 这个访问结果表示当前的遍历过程将会停止。

public interface FileVisitor<T> {
    FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
        throws IOException;

    FileVisitResult visitFile(T file, BasicFileAttributes attrs)
        throws IOException;

    FileVisitResult visitFileFailed(T file, IOException exc)
        throws IOException;

    FileVisitResult postVisitDirectory(T dir, IOException exc)
        throws IOException;
}

通过它去遍历文件树会比较方便,比如查找文件夹内符合某个条件的文件或者某一天内所创
建的文件,这个类中都提供了相对应的方法。我们来看一下它的实现其实也非常简单:

public class SimpleFileVisitor<T> implements FileVisitor<T> {
    protected SimpleFileVisitor() {
    }

    @Override
    public FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
        throws IOException
    {
        Objects.requireNonNull(dir);
        Objects.requireNonNull(attrs);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFile(T file, BasicFileAttributes attrs)
        throws IOException
    {
        Objects.requireNonNull(file);
        Objects.requireNonNull(attrs);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFileFailed(T file, IOException exc)
        throws IOException
    {
        Objects.requireNonNull(file);
        throw exc;
    }

    @Override
    public FileVisitResult postVisitDirectory(T dir, IOException exc)
        throws IOException
    {
        Objects.requireNonNull(dir);
        if (exc != null)
            throw exc;
        return FileVisitResult.CONTINUE;
    }
}

下面再来看访问者模式在Spring中的应用, Spring loC中有个BeanDefinitionVisitor类 , 其中有一个visitBeanDefinitionO方 法 ,我们来看他的源码:

public class BeanDefinitionVisitor {

   @Nullable
   private StringValueResolver valueResolver;

   public BeanDefinitionVisitor(StringValueResolver valueResolver) {
      Assert.notNull(valueResolver, "StringValueResolver must not be null");
      this.valueResolver = valueResolver;
   }

   protected BeanDefinitionVisitor() {
   }

   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方法中,分别访问了其他的数据,比如父类的名字、自己 的类名、在 loC容器中的名称等各种信息。

22.5.解释器模式的优缺点

优点:

1、 解耦了数据结构与数据操作,使得操作集合可以独立变化;

2、 扩展性好:可以通过扩展访问者角色,实现对数据集的不同操作;

3、 元素具体类型并非单一,访问者均可操作;

4、 各角色职责分离,符合单一职责原则。

缺点:

1、无法增加元素类型:若系统数据结构对象易于变化,经常有新的数据对象增加进来,则访问
者类必须增加对应元素类型的操作,违背了开闭原则;

2、具体元素变更困难:具体元素增加属性,删除属性等操作会导致对应的访问者类需要进行相
应的修改,尤其当有大量访问者类时,修改范围太大;

3、违背依赖倒置原则:为了达到" 区别对待〃,访问者依赖的是具体元素类型,而不是抽象。

22.6.思维导图

image-20200325134227908

22.7.作业

1、用Guava API实现GPer社区提问通知的业务场景。

2、说说你理解的访问者模式的精髓是什么?

发布了26 篇原创文章 · 获赞 4 · 访问量 2427

猜你喜欢

转载自blog.csdn.net/u012138605/article/details/105093382