【设计模式】访问者模式

770 阅读5分钟

本文主要介绍访问者模式的概念及用法。

模式背景

在系统,有时候一系列对象需要被以不同的方式访问。比如医院开的药方,在医院财务那这个药房只是用来计算价格,在抓药那里这个药方是用来抓药的。类比到实际项目使用中:一个集合里面可能会有几类元素,不同类型的元素可以有不同的访问者访问他,并且有不同的访问执行逻辑。如果没有一个好的模式支持,那么我们可能需要在某个类中使用大量的if-else来对不同访问者进行判别来进行分开处理。但是这样问题也很明显:类的职责重,大量的if-else代码一定不优雅,一旦访问者或者被访问对象需要修改/添加/移除等操作,这种方式基本就毫无扩展性而言。所以这时候如何很好的添加新的访问者而不需要修改代码就是访问者模式提供的。

定义&概念

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

原理

访问者模式是一个比较复杂的设计模式。整体结构我们可以分为2层:访问层,元素层(也可以说是被访问层)。其核心的思想还是去除if-else将各个不同的操作封装到不同的访问者对象类中去。为了方便扩展,对元素和访问者都提供了抽象层,而元素层,我们还需要通过一个对象结构(可以就理解为是一个集合,来定义这些元素的结构的)来对元素的结构进行组织。

主要看看他们的组成元素,注意里面最精髓的2个地方就是:acceptvisit方法。

组成要素

  • 抽象访问者(Visitor)
    • 主要是为了访问者的扩展。
    • 里面定义了一系列的visit方法(这个方法可以通过方法名来区分,也可以使用参数来重载),用来指定不同元素对象该访问者所需要对应的不同操作逻辑。一般就一个元素提供一个visit方法。
  • 具体访问者(ConcreteVisitor)
    • 实现抽象的访问逻辑。
  • 抽象元素(Element)
    • 定义一个accept方法,参数通常就是抽象访问者。
  • 具体元素(ConcreteElement)
    • 实现accept完成对一个元素访问想要做的操作。本质就是调用传递来的抽象访问者的方法(即调用visitor的visit方法)。
    • 这种机制也称为双重分派,这样利用参数重载的特性,我们可以不用修改任何代码,添加的访问者都可以这种方式进行传递调用(通过参数重载来调用对应的方法)。
  • 对象结构(ObjectStructure)
    • 用来存放元素对象的。并提供遍历内部元素的方法。
    • 可以使用组合模式来实现。也可以是一个简单的集合对象。

概括一下他的思想:一个叫对象结构的东西来封装集合的结构,然后具体的元素抽出去,形成一个体系。访问者又是另外一个体系。 访问者体系中,每一个访问者都要对需要的操作实现一个visit方法,这个方法将来通过将自己传递给具体的元素对象,然后通过元素对象来进行调用!

其复杂性也正是在于visit和accept方法的设计上,确实很精巧,但也确实让结构变得没那么直观。

UML

实现

抽象访问者

public interface Visitor {
    void visit(ConcreteElementA concreteElementA);
    void visit(ConcreteElementB concreteElementB);
}

具体访问者

public class ConcreteVisitorA implements Visitor {
    @Override
    public void visit(ConcreteElementA concreteElementA) {
        System.out.println("A visit:" + concreteElementA.getName());
    }

    @Override
    public void visit(ConcreteElementB concreteElementB) {
        System.out.println("A visit:" +concreteElementB.getName());
    }
}

抽象元素

public interface Element {
    /**
     * 这accept对该类的元素传入一个accept。则集合中该元素都会使用该访问者处理。
     * 而这个访问者也实现了visit该元素的方法。所以传入this就可以重载到对应实现逻辑里面
     * @param visitor
     */
    void accept(Visitor visitor);
}

具体元素

public class ConcreteElementA implements Element {

    private String name;

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

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    public String getName() {
        return name;
    }
}

对象结构

public class ObjectStructure {

    /**
     * 使用集合集合来存放所有的元素。
     */
    private List<Element> list = new ArrayList<>();

    public void addElement(Element e) {
        list.add(e);
    }

    public void removeElement(Element e) {
        list.remove(e);
    }

    /**
     * 使用某个访问者来遍历集合元素
     */
    public void accept(Visitor visitor) {
        list.forEach(item -> item.accept(visitor));
    }
}

使用

Element e1, e2, e3, ee1, ee2, ee3;
ObjectStructure objectStructure = new ObjectStructure();
e1 = new ConcreteElementA("A1");
e2 = new ConcreteElementA("A2");
e3 = new ConcreteElementA("A3");
ee1 = new ConcreteElementA("B1");
ee2 = new ConcreteElementA("B2");
ee3 = new ConcreteElementA("B3");
objectStructure.addElement(e1);
objectStructure.addElement(e2);
objectStructure.addElement(e3);
objectStructure.addElement(ee1);
objectStructure.addElement(ee2);
objectStructure.addElement(ee3);
//可以通过配置来获取使用哪个访问类
Visitor v = new ConcreteVisitorA();
objectStructure.accept(v);

优缺点

优点

  • 各角色的职责相互隔离,符合单一职责原则。
    • Visitor、Element 、ObjectStructure各司其责,职责清晰。
  • 扩展性好,添加新的访问者不需要修改原代码,对于访问者来说,符合开闭原则。

缺点

  • 不能解决添加一个元素类的问题,一旦添加一个元素类,就要修改所有相关的访问者,这一点违背了开闭原则。
  • 可能破坏元素类的封装性,因为该模式需要访问者对象去调用元素对象的具体操作,所以可能需要元素对象暴露自己一些内部状态,来配合访问者对象一起完成操作。

使用场景

访问者使用的条件较为苛刻,结构也很复杂,所以实际应用使用的频率不高。当你系统中存在一个比较复杂的对象结构,并且存在着不同的访问者并对其访问的操作也不同的时候,可以使用访问者模式。

现有的一些实际应用:XML文档解析,编译器设计等。

总结

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

相关代码:github.com/zhaohaoren/…

如有代码和文章问题,还请指正!感谢!