设计模式 - 访问者模式

405 阅读10分钟

✨作者:猫十二懿

❤️‍🔥账号:CSDN掘金个人博客Github

🎉公众号:猫十二懿

访问者模式

1、访问者模式介绍

访问者模式(Visitor Pattern)是一种行为型设计模式,它允许你在不修改现有对象结构的情况下定义一些新操作。通过将操作封装在一个访问者对象中,可以让你在不改变被访问对象的类的前提下,定义对该对象的新操作。

访问者模式的核心思想是将数据结构与数据操作分离。它适用于那些数据结构稳定,但需要经常添加新的操作的场景。通过引入访问者模式,可以避免在已有的数据结构中添加新操作时的修改和维护工作。

1.1 访问者模式的基本实现

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

访问者模式结构图:

image-20230524210544091

访问者模式包含以下几个关键角色:

  1. 访问者(Visitor):定义了对每个元素的具体访问操作,它通过重载访问者的方法来实现不同的操作。
  2. 具体访问者(ConcreteVisitor):实现了访问者定义的操作,并针对具体的元素类型提供了不同的访问实现。
  3. 元素(Element):定义了一个接受访问者的方法,该方法将访问者作为参数传入,使访问者能够访问该元素。
  4. 具体元素(ConcreteElement):实现了元素接口,具体元素中会包含接受访问者的具体实现,即调用访问者的方法。
  5. 对象结构(Object Structure):包含元素的集合,提供了一个可以被访问的集合接口。

访问者模式的使用步骤如下:

  1. 定义访问者接口,该接口声明了多个访问方法,每个方法对应一个具体元素的访问操作。
  2. 定义具体访问者类,实现访问者接口,并为每个具体元素提供不同的访问实现。
  3. 定义元素接口,声明接受访问者的方法。
  4. 定义具体元素类,实现元素接口,并在接受访问者的方法中调用访问者的相应方法。
  5. 定义对象结构类,该类维护一个元素集合,并提供遍历元素集合的方法,将访问者作为参数传入元素的接受方法。

Visitor类,为该对象结构中ConcreteElement的每一个类声明一个Visit 操作。

/**
 * @author Shier
 * CreateTime 2023/5/24 21:10
 * 抽象访问者类
 */
public abstract class Visitor {
    public abstract void visitConcreteElementA(ConcreteElementA concreteElementA);
    
    public abstract void visitConcreteElementB(ConcreteElementB concreteElementB);
}

ConcreteVisitor1和ConcreteVisitor2类:具体访问者,实现每个由 Visitor声明的操作。每个操作实现算法的一部分,而该算法片断乃是对应于 结构中对象的类。

/**
 * @author Shier
 * CreateTime 2023/5/24 21:17
 */
public class ConcreteVisitor1 extends Visitor {
    @Override
    public void visitConcreteElementA(ConcreteElementA concreteElementA) {
        System.out.println(concreteElementA.getClass().getSimpleName() + "被:" + this.getClass().getSimpleName() + "访问了");
    }

    @Override
    public void visitConcreteElementB(ConcreteElementB concreteElementB) {
        System.out.println(concreteElementB.getClass().getSimpleName() + "被:" + this.getClass().getSimpleName() + "访问了");
    }
}

/**
 * @author Shier
 * 具体访问者B
 */
public class ConcreteVisitor2 extends Visitor {

    @Override
    public void visitConcreteElementA(ConcreteElementA concreteElementA) {
        System.out.println(concreteElementA.getClass().getSimpleName() + "被:" + this.getClass().getSimpleName() + "访问了");
    }

    @Override
    public void visitConcreteElementB(ConcreteElementB concreteElementB) {
        System.out.println(concreteElementB.getClass().getSimpleName() + "被:" + this.getClass().getSimpleName() + "访问了");
    }
}

Element类:定义一个Accept操作,它以一个访问者为参数。

/**
 * @author Shier
 * CreateTime 2023/5/24 21:08
 */
public abstract class Element {
    public abstract void accept(Visitor visitor);
}

ConcreteElementA和ConcreteElementB类:具体元素,实现Accept操作。

/**
 * @author Shier
 * CreateTime 2023/5/24 21:17
 * 具体A元素操作
 */
public class ConcreteElementA extends Element{
    @Override
    public void accept(Visitor visitor) {
        // 使用双分派技术,实现处理与数据结构的分离
        visitor.visitConcreteElementA(this);
    }

    /**
     * 其他操作
     */
    public void operateA(){}
}
/**
 * @author Shier
 * CreateTime 2023/5/24 21:17
 * 具体B元素操作
 */
public class ConcreteElementB extends Element{
    @Override
    public void accept(Visitor visitor) {
        visitor.visitConcreteElementB(this);
    }

    /**
     * 其他操作
     */
    public void operateB(){}
}

ObjectStructure类:能枚举它的元素,可以提供一个高层的接口以允许访问者访问它的元素。

/**
 * @author Shier
 * CreateTime 2023/5/24 21:20
 */
public class ObjectStructure {
    private ArrayList<Element> elements = new ArrayList<>();

    public void attach(Element element) {
        elements.add(element);
    }

    public void detach(Element element) {
        elements.remove(element);
    }

    public void accept(Visitor visitor) {
        for (Element element : elements) {
            element.accept(visitor);
        }
    }
}

客户端:

/**
 * @author Shier
 * CreateTime 2023/5/24 21:23
 */
public class VisitorClint {
    public static void main(String[] args) {
        ObjectStructure structure = new ObjectStructure();
        structure.attach(new ConcreteElementA());
        structure.attach(new ConcreteElementB());

        ConcreteVisitor1 visitor1 = new ConcreteVisitor1();
        ConcreteVisitor2 visitor2 = new ConcreteVisitor2();

        structure.accept(visitor1);
        structure.accept(visitor2);
    }
}

最终的输出结果:

image-20230524213349894

2、具体例子 - 男人与女人

人类只分为男人和女人,才会有这么多的对比。

2.1 不使用访问者模式实现男人与女人之间的比较

人类:男人和女人类的抽象类

/**
 * @author Shier
 * CreateTime 2023/5/24 21:36
 * 人抽象类
 */
public abstract class Person {
    protected String action;

    public String getAction() {
        return this.action;
    }

    public void setAction(String value) {
        this.action = value;
    }

    /**
     * 得到结论或反应
     */
    public abstract void getConclusion();
}

男人类:

/**
 * @author Shier
 * CreateTime 2023/5/24 21:37
 * 男人
 */
public class Man extends Person {

    /**
     * 得到结论或反应
     */
    public void getConclusion() {
        if (action == "成功") {
            System.out.println(this.getClass().getSimpleName() + this.action + "时,背后多半有一个伟大的女人。");
        } else if (action == "失败") {
            System.out.println(this.getClass().getSimpleName() + this.action + "时,闷头喝酒,谁也不用劝。");
        } else if (action == "恋爱") {
            System.out.println(this.getClass().getSimpleName() + this.action + "时,凡事不懂也要装懂。");
        }
    }
}

女人类:

/**
 * @author Shier
 * CreateTime 2023/5/24 21:37
 * 女人
 */
public class Woman extends Person {

    /**
     * 得到结论或反应
     */

    public void getConclusion() {
        if (action == "成功") {
            System.out.println(this.getClass().getSimpleName() + this.action + "时,背后大多有一个不成功的男人。");
        } else if (action == "失败") {
            System.out.println(this.getClass().getSimpleName() + this.action + "时,眼泪汪汪,谁也劝不了。");
        } else if (action == "恋爱") {
            System.out.println(this.getClass().getSimpleName() + this.action + "时,遇事懂也装作不懂。");
        }
    }
}

客户端:

public class Test {

    public static void main(String[] args) {
        ArrayList<Person> persons = new ArrayList<Person>();
        Person man1 = new Man();
        man1.setAction("成功");
        persons.add(man1);
        Person woman1 = new Woman();
        woman1.setAction("成功");
        persons.add(woman1);

        Person man2 = new Man();
        man2.setAction("失败");
        persons.add(man2);

        Person woman2 = new Woman();
        woman2.setAction("失败");
        persons.add(woman2);

        Person man3 = new Man();
        man3.setAction("恋爱");
        persons.add(man3);
        Person woman3 = new Woman();
        woman3.setAction("恋爱");
        persons.add(woman3);

        for (Person item : persons) {
            item.getConclusion();
        }
    }
}

输出结果:

image-20230524214157059

如果我现在要增加一个“结婚”的状态,就需要改这两个类(Man、Woman)都需要增加分支判断。下面就使用访问者模式实现这个过程

2.2 使用访问者模式 - 男人与女人

结构图如下:

image-20230524215140915

状态类的抽象类、人的抽象类:

/**
 * @author Shier
 * CreateTime 2023/5/24 21:45
 * 人类抽象类
 */
public abstract class Person {
    //接受
    public abstract void accept(Action visitor);
}
/**
 * @author Shier
 * CreateTime 2023/5/24 21:45
 * 状态抽象类
 */
public abstract class Action {
    /**
     * 得到男人结论或反应
     *
     * @param concreteElementA
     */
    public abstract void getManConclusion(Man concreteElementA);

    /**
     * 得到女人结论或反应
     *
     * @param concreteElementB
     */
    public abstract void getWomanConclusion(Woman concreteElementB);
}

这里关键就在于人只分为男人和女人,这个性别的分类是稳定的,所以可以在状态类中,增加'男人反应'和'女人反应'两个方法,方法个数是稳定的,不会很容易地发生变化。 而 '人' 抽象类中有一个抽象方法 '接受' (accept) ,它是用来获得 '状态' 对象的。每一种具体状态都继承 '状态' 抽象类,实现两个反应的方法。

具体状态类:

/**
 * @author Shier
 * CreateTime 2023/5/24 21:46
 * 成功类
 */
public class Success extends Action{
    public void getManConclusion(Man concreteElementA){
        System.out.println(concreteElementA.getClass().getSimpleName()
                +" "+this.getClass().getSimpleName()+"时,背后多半有一个伟大的女人。");
    }

    public void getWomanConclusion(Woman concreteElementB){
        System.out.println(concreteElementB.getClass().getSimpleName()
                +" "+this.getClass().getSimpleName()+"时,背后大多有一个不成功的男人。");
    }
}
/**
 * @author Shier
 * CreateTime 2023/5/24 21:46
 * 失败类
 */
public class Failing extends Action {
    public void getManConclusion(Man concreteElementA){
        System.out.println(concreteElementA.getClass().getSimpleName()
                +" "+this.getClass().getSimpleName()+"时,闷头喝酒,谁也不用劝。");
    }

    public void getWomanConclusion(Woman concreteElementB){
        System.out.println(concreteElementB.getClass().getSimpleName()
                +" "+this.getClass().getSimpleName()+"时,眼泪汪汪,谁也劝不了。");
    }
}
/**
 * @author Shier
 * CreateTime 2023/5/24 21:46
 * 恋爱类
 */
public class Amativeness extends Action {
    public void getManConclusion(Man concreteElementA){
        System.out.println(concreteElementA.getClass().getSimpleName()
                +" "+this.getClass().getSimpleName()+"时,凡事不懂也要装懂。");
    }

    public void getWomanConclusion(Woman concreteElementB){
        System.out.println(concreteElementB.getClass().getSimpleName()
                +" "+this.getClass().getSimpleName()+"时,遇事懂也装作不懂。");
    }
}

男人和女人类:

/**
 * @author Shier
 * CreateTime 2023/5/24 21:45
 * 男人类
 */
public class Man extends Person{
    public void accept(Action visitor) {
        visitor.getManConclusion(this);
    }
}
/**
 * @author Shier
 * CreateTime 2023/5/24 21:46
 * 女人类
 */
public class Woman extends Person{
    public void accept(Action visitor) {
        visitor.getWomanConclusion(this);
    }
}

这里需要提一下当中用到一种双分派的技术,首先在客户程序中将具体状态作为参数传递给 男人类完成了一次分派,然后 男人 类调用作为参数的 具体状态 中的方法 男人反应,同时将自己(this)作为参数传递进去。

这便完成了第二次分派。双分派意味着得到执行的操作决定于请求的种类和两个接收者的类型。接受方法就是一个双分派的操作,它得到执行的操作不仅决定于状态类的具体状态,还决定于它访问的人的类别。

由于总是需要男人与女人在不同状态的对比,所以我们需要一 个对象结构类来针对不同的状态遍历男人与女人,得到不同的反应。

/**
 * @author Shier
 * CreateTime 2023/5/24 21:46
 * 对象结构
 */
public class ObjectStructure {
    private ArrayList<Person> elements = new ArrayList<Person>();

    //增加
    public void attach(Person element) {
        elements.add(element);
    }
    //移除
    public void detach(Person element) {
        elements.remove(element);
    }
    //查看显示
    public void display(Action visitor) {
        for(Person e : elements) {
            e.accept(visitor);
        }
    }
}

客户端:

/**
 * @author Shier
 * CreateTime 2023/5/24 21:56
 */
public class Test {
    public static void main(String[] args) {
        ObjectStructure o = new ObjectStructure();
        o.attach(new Man());
        o.attach(new Woman());

        //成功时的反应
        Success v1 = new Success();
        o.display(v1);


        //失败时的反应
        Failing v2 = new Failing();
        o.display(v2);

        //恋爱时的反应
        Amativeness v3 = new Amativeness();
        o.display(v3);

    }
}

最终结果:

image-20230524215946883

现在我们,再来增加一个结婚状态,作为 男人和女人的反应。

由于用了双分派,使得我只需要增加一个 '状态' 子类,就可以在客户端调用来查看,不需要改动其他任何类的代码。

新增一个结婚状态:

/**
 * @author Shier
 * CreateTime 2023/5/24 21:50
 * 结婚
 */
public class Marriage extends Action {
    public void getManConclusion(Man concreteElementA) {
        System.out.println(concreteElementA.getClass().getSimpleName()
                + " " + this.getClass().getSimpleName() + "时,感慨道:恋爱游戏终结时,‘有妻徒刑’遥无期。");
    }

    public void getWomanConclusion(Woman concreteElementB) {
        System.out.println(concreteElementB.getClass().getSimpleName()
                + " " + this.getClass().getSimpleName() + "时,欣慰曰:爱情长跑路漫漫,婚姻保险保平安。");
    }
}

再去修改客户端:

//婚姻时的反应
Marriage v4 = new Marriage();
o.display(v4);

完美地体现了开放-封闭原则,访问者模式应该算是GoF中最复杂的一个模式。

GoF:GoF 是指《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)一书的作者集合,也被称为四人组(Gang of Four,GoF)。这本书由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四位软件工程师共同撰写。

3、访问者模式总结

3.1 访问者模式缺点

  1. 增加新元素困难:如果需要增加新的元素类,就需要修改所有的访问者类,使其能够访问新的元素类。这违反了开闭原则,可能会导致代码修改的工作量较大。
  2. 破坏封装:访问者模式将具体元素的访问操作放到了访问者类中,可能会破坏元素类的封装性。

3.2 访问者模式优点

  1. 分离数据结构与操作:访问者模式将数据结构与具体的操作分离开来,使得新增操作时无需修改现有对象结构,符合开闭原则。
  2. 增加新操作方便:通过新增具体访问者类,可以很容易地增加新的操作,而无需修改现有的元素类
  3. 提高扩展性:由于新增操作是通过添加具体访问者类实现的,所以在不修改元素类的情况下,可以灵活地扩展和变化操作

3.3 访问者模式使用场景

  1. 对象结构稳定但需要频繁添加新操作:如果对象结构相对稳定,但需要对其进行多种不同的操作,可以考虑使用访问者模式。这样可以避免修改对象结构的代码,而只需要添加新的访问者类即可。
  2. 对象结构中的元素类型较少,但操作类型多变:当对象结构中的元素类型较少,但操作类型频繁变化时,可以使用访问者模式。通过将不同操作封装在具体访问者类中,可以方便地增加、修改和组合不同的操作。
  3. 数据结构与操作分离:如果需要对一组对象进行复杂的操作,且不希望这些操作污染对象的类,可以考虑使用访问者模式。通过将操作封装在访问者类中,实现了数据结构与操作的分离,使得代码更加清晰和可维护。