访问者模式--年轻人和中年人的代沟

2022-05-16 14:19:13 浏览数 (2)

引子

小帅25岁,老王35岁,有一天小帅和老王讨论起了现在年轻和中年人之间的差异越来越明显的问题。

小帅列出了几种情景,同时小帅作为年轻人的代表,老王作为中年人的代表分别作出了回答。

  1. 工作遇到不顺心的事,又不能马上解决,比如领导对你有偏见或者工作任务太繁重怎么办? 小帅:此处不留爷,自有留爷处,立马换工作。 老王:先忍忍,再熬熬,说不定有时来运转的一天,毕竟每个月还有这么多房贷要还呢。
  2. 发了年终奖,怎么安排? 小帅:去买个最新款的手机或者其他电子产品,好好爽一爽,今朝有酒今朝醉。 老王:全部拿去还房贷。
  3. 周末怎么过?

小帅:两件事,睡觉,打游戏。

老王:两件事,干家务,带娃。

怪不得年轻人和中年人玩不到一块去,这就是代沟啊。

老王说:我们说了这么多,你能不能用代码把上面的对话写出来?

小帅:真是服了你了,什么事都能扯到代码上,行,今天我心情好,写给你看看。

人的抽象类:

代码语言:javascript复制
/**
 * 人的抽象类
 */
public abstract class Person {
    /**
     * 场景
     */
    protected String scene;

    public Person(String scene) {
        this.scene = scene;
    }

    /**
     * 回答
     */
    public abstract void answer();
}

年轻人类:

代码语言:javascript复制
/**
 * 年轻人
 */
public class Young extends Person{

    public Young(String scene) {
        super(scene);
    }

    @Override
    public void answer() {
        if("工作不顺利".equals(scene)) {
            System.out.println("年轻人:此处不留爷,自有留爷处,立马换工作。");
        } else if("年终奖".equals(scene)) {
            System.out.println("年轻人:去买个最新款的手机或者其他电子产品,好好爽一爽,今朝有酒今朝醉。");
        } else if("周末".equals(scene)) {
            System.out.println("年轻人:两件事,睡觉,打游戏。");
        }
    }
}

中年人类:

代码语言:javascript复制
/**
 * 中年人
 */
public class MiddleAged extends Person{

    public MiddleAged(String scene) {
        super(scene);
    }

    @Override
    public void answer() {
        if("工作不顺利".equals(scene)) {
            System.out.println("中年人:先忍忍,再熬熬,说不定有时来运转的一天,毕竟每个月还有这么多房贷要还呢。");
        } else if("年终奖".equals(scene)) {
            System.out.println("中年人:全部拿去还房贷。");
        } else if("周末".equals(scene)) {
            System.out.println("中年人:两件事,干家务,带娃。");
        }
    }
}

客户端类:

代码语言:javascript复制
/**
 * 客户端
 */
public class Client {

    public static void main(String[] args) {
        List<Person> personList = new ArrayList<>();
        personList.add(new Young("工作不顺利"));
        personList.add(new MiddleAged("工作不顺利"));
        personList.add(new Young("年终奖"));
        personList.add(new MiddleAged("年终奖"));
        personList.add(new Young("周末"));
        personList.add(new MiddleAged("周末"));

        personList.stream().forEach(f -> f.answer());
    }
}

输出:

代码语言:javascript复制
年轻人:此处不留爷,自有留爷处,立马换工作。
中年人:先忍忍,再熬熬,说不定有时来运转的一天,毕竟每个月还有这么多房贷要还呢。
年轻人:去买个最新款的手机或者其他电子产品,好好爽一爽,今朝有酒今朝醉。
中年人:全部拿去还房贷。
年轻人:两件事,睡觉,打游戏。
中年人:两件事,干家务,带娃。

老王看罢,说道:写得不错,不过还有些问题,如果我要再增加几个问题场景,比如:平时看什么书、喜欢什么运动、晚上几点睡之类的问题怎么办呢?

小帅:这还不简单,直接在Young和MiddleAged类中加上if...else条件就行了啊!

老王摇摇头道:这样就违反了开闭原则:对修改关闭,对扩展开放。 如果这是第三方提供的类,你不能修改它们,但要增加新的行为,该怎么做呢?

“在不修改已有代码的情况下,向已有的类增加新的行为?这怎么可能?”小帅怀疑道。

老王笑道:怎么不可能,有个设计模式就是干这个的。

访问者模式

访问者模式:提供一个作用于某种对象结构中的各元素的操作,可以在不改变元素类的前提下,定义作用于元素的新操作。

访问者模式是一种行为设计模式,允许你在不修改已有代码的情况下向已有类层次结构中增加新的行为。

  • Visitor(访问者,如Scene) 抽象类或者接口,声明访问者可以访问哪些元素。具体到程序中就是getYoungAnswer方法能访问young对象;getMiddleAgedAnswer方法能访问middleAged对象。
  • ConcreteVisitor(具体访问者,如WorkNotWell,YearEndAwards,Weekend) 访问者接口的实现类,实现具体的操作。
  • Element(元素,如Person) 接口或者抽象类,定义一个accept操作,以一个访问者为参数。
  • ConcreteElement(具体元素,如Young和MiddleAged) 实现accept操作,通常是visitor.visit(this)模式。
  • ObjectStructure(对象结构) 存储对象的集合,方便遍历其中的元素。

场景类:

代码语言:javascript复制
/**
 * 场景
 */
public interface Scene {

    /**
     * 年轻人的回答
     * @param young
     */
    public void getYoungAnswer(Young young);

    /**
     * 中年人的回答
     * @param middleAged
     */
    public void getMiddleAgedAnswer(MiddleAged middleAged);
}

工作不顺场景:

代码语言:javascript复制
/**
 * 工作不顺
 */
public class WorkNotWell implements Scene{

    @Override
    public void getYoungAnswer(Young young) {
        System.out.println(young.name   ":此处不留爷,自有留爷处,立马换工作。");
    }

    @Override
    public void getMiddleAgedAnswer(MiddleAged middleAged) {
        System.out.println(middleAged.name   ":先忍忍,再熬熬,说不定有时来运转的一天,毕竟每个月还有这么多房贷要还呢。");
    }
}

年终奖场景:

代码语言:javascript复制
/**
 * 年终奖
 */
public class YearEndAwards implements Scene{

    @Override
    public void getYoungAnswer(Young young) {
        System.out.println(young.name   ":去买个最新款的手机或者其他电子产品,好好爽一爽,今朝有酒今朝醉。");
    }

    @Override
    public void getMiddleAgedAnswer(MiddleAged middleAged) {
        System.out.println(middleAged.name   ":全部拿去还房贷。");
    }
}

周末场景:

代码语言:javascript复制
/**
 * 周末
 */
public class Weekend implements Scene{

    @Override
    public void getYoungAnswer(Young young) {
        System.out.println(young.name   ":两件事,睡觉,打游戏。");
    }

    @Override
    public void getMiddleAgedAnswer(MiddleAged middleAged) {
        System.out.println(middleAged.name   ":两件事,干家务,带娃。");
    }
}

人的接口:

代码语言:javascript复制
/**
 * 人的接口
 */
public interface Person {

    /**
     * 接受
     */
    public void accept(Scene scene);
}

年轻人类:

代码语言:javascript复制
/**
 * 年轻人
 */
public class Young implements Person{

    protected String name;

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

    @Override
    public void accept(Scene scene) {
        scene.getYoungAnswer(this);
    }
}

中年人类:

代码语言:javascript复制
/**
 * 中年人
 */
public class MiddleAged implements Person{

    protected String name;

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

    @Override
    public void accept(Scene scene) {
        scene.getMiddleAgedAnswer(this);
    }
}

对象结构类:

代码语言:javascript复制
/**
 * 对象结构
 */
public class ObjectStructure {

    private List<Person> personList = new ArrayList<>();

    /**
     * 新增
     * @param person
     */
    public void add(Person person) {
        personList.add(person);
    }

    /**
     * 删除
     * @param person
     */
    public void delete(Person person) {
        personList.remove(person);
    }

    /**
     * 遍历显示
     * @param scene
     */
    public void display(Scene scene) {
        personList.stream().forEach(f -> f.accept(scene));
    }
}

客户端类:

代码语言:javascript复制
/**
 * 客户端
 */
public class Client {
    public static void main(String[] args) {
        ObjectStructure objectStructure = new ObjectStructure();
        objectStructure.add(new Young("小帅"));
        objectStructure.add(new MiddleAged("老王"));

        // 工作不顺利的场景
        WorkNotWell workNotWell = new WorkNotWell();
        objectStructure.display(workNotWell);

        // 年终奖的场景
        YearEndAwards yearEndAwards = new YearEndAwards();
        objectStructure.display(yearEndAwards);

        // 周末的场景
        Weekend weekend = new Weekend();
        objectStructure.display(weekend);
    }
}

输出:

代码语言:javascript复制
小帅:此处不留爷,自有留爷处,立马换工作。
老王:先忍忍,再熬熬,说不定有时来运转的一天,毕竟每个月还有这么多房贷要还呢。
小帅:去买个最新款的手机或者其他电子产品,好好爽一爽,今朝有酒今朝醉。
老王:全部拿去还房贷。
小帅:两件事,睡觉,打游戏。
老王:两件事,干家务,带娃。

老王对小帅说道:应用访问者模式就能实现,在不修改已有代码的情况下,向已有的类增加新的行为。

你看比如我要新增”晚上几点睡“的场景,只需要要新增一个Sleep类实现Scene接口就行了。

代码语言:javascript复制
/**
 * 睡觉
 */
public class Sleep implements Scene{
    @Override
    public void getYoungAnswer(Young young) {
        System.out.println(young.name   ":十二点半才睡,精力旺盛。");
    }

    @Override
    public void getMiddleAgedAnswer(MiddleAged middleAged) {
        System.out.println(middleAged.name   ":十点半就睡,早睡早起身体好。");
    }
}

然后在Client类中增加相关的调用代码,就能实现这个场景了:

代码语言:javascript复制
// 睡觉的场景
Sleep sleep = new Sleep();
objectStructure.display(sleep);
代码语言:javascript复制
小帅:十二点半才睡,精力旺盛。
老王:十点半就睡,早睡早起身体好。

你看,根本不需要修改Young和MiddleAged类就能增加新的操作,是不是很神奇?

小帅有点迷惑:是挺有意思的,不过这段代码看上去有点奇怪,accept方法为什么要把scene对象传进来,然后又把自己的对象this传进去呢?

这不就相当于我自己调用别人的方法,把自己搭进去了?

代码语言:javascript复制
@Override
public void accept(Scene scene) {
    scene.getYoungAnswer(this);
}

单分派和双分派

老王哈哈大笑道:“把自己搭进去”,小帅你这个说法倒挺搞笑的,我给你讲讲具体是怎么回事。

三个场景WorkNotWell,Weekend,YearEndAwards是如何和Young,MiddleAged关联的呢?

其实中间分了两步走:

第一,在调用display方法的时候,传入了具体的场景实现类,确定了具体的场景。

第二、在accept方法中传入了this对象,具体的元素对象(Young)也确定了,这样两个对象就都确定了。

这里,我还要和你讲一下单分派(Single Dispatch)和双分派(Double Dispatch)的概念,老王接着说。

所谓单分派,指的是执行哪个对象的方法,根据对象的运行时类型来决定执行对象的哪个方法,根据方法参数的编译时类型来决定

所谓双分派,指的是执行哪个对象的方法,根据对象的运行时类型来决定执行对象的哪个方法,根据方法参数的运行时类型来决定

我们先来看一段代码:

代码语言:javascript复制
public class Parent {
    public void f() {
        System.out.println("我是父类的方法f()");
    }
}
public class Child extends Parent{
    @Override
    public void f() {
        System.out.println("我是子类的方法f()");
    }
}

/**
 * 单分派
 */
public class SingleDispatch {
    public void show(Parent p) {
        p.f();
    }
    public void overloadFunction(Parent p) {
        System.out.println("我是父类参数重载方法:overloadFunction(Parent p)");
    }
    public void overloadFunction(Child c) {
        System.out.println("我是子类参数重载方法:overloadFunction(Child c)");
    }
}

public class Test {
    public static void main(String[] args){
        SingleDispatch singleDispatch = new SingleDispatch();
        Parent p = new Child();
        singleDispatch.show(p);
        singleDispatch.overloadFunction(p);
    }
}

输出:

代码语言:javascript复制
我是子类的方法f()
我是父类参数重载方法:overloadFunction(Parent p)

单分派的图解:

由于Java是单分派语言,所以执行哪个对象的方法,根据对象的运行时类型来决定,p.f()这里的p运行的是Child的对象,所以执行的是Child类中的方法。

由于执行对象的哪个方法,根据方法参数的编译时类型来决定,这里编译时的类型为Parent。

所以调用的是public void overloadFunction(Parent p)方法。

由于Java语言只支持单分派,所以要用访问者模式实现双分派,这就是为什么在ConcreteVisitor类中要用ConcreteElement的实现类,而不能用Element接口。

然后通过在accept方法中传入this对象,来确定调用的方法。

代码语言:javascript复制
@Override
public void accept(Scene scene) {
    scene.getYoungAnswer(this);
}

总结

如果一个对象结构比较复杂,同时元素稳定不易变化,即ConcreteElement类比较稳定,不会会随便增加,但却需要经常在此结构上定义新的操作,那就非常合适使用访问者模式。

优点

  • 符合开闭原则,在不修改已有代码的情况下,向已有的类增加新的行为。
  • 将有关的行为集中到一个访问者对象中,简化了元素类。

缺点

  • 增加新的ConcreteElement类比较麻烦,每新增加一个新的ConcreteElement类,就要在所有的Visitor类中增加新的操作。

如果总有新的ConcreteElement类加入进来,Vistor类和它的子类将变的难以维护,这种情况下就适合用访问者模式了。

访问者模式使我们更加容易的增加访问操作,但增加元素比较困难,所以访问者模式适用于元素比较稳定的结构。

0 人点赞