JAVA设计模式之访问者模式

前言

本系列文章参考《设计模式之禅》、菜鸟教程网以及网上的一些文章进行归纳总结,并结合自身开发应用。设计模式的命名以《设计模式之禅》为准。

设计模式仅是一些开发者在日常开发中的编码技巧的汇总并非固定不变,可根据项目业务实际情况进行扩展和应用,切不可被这个束缚。更不要为了使用而使用,设计模式是一把双刃剑,过度的设计会导致代码的可读性下降,代码的体积增加。

系列文章不会详细介绍设计模式的《七大原则》,也不会对设计模式进行分类。这样只会增加学习和记忆的成本,也会导致使用时的思想固化,总在想这么设计是否符合xx原则,是否是xx设计模式,xx模式是什么类型等等,不是本系列文章的所希望看到的,目标只有一个,结合日常开发分享自身的应用,以提供一种代码优化的思路。

学习然后忘记,也许是一种最好的方式。

就像俗话说的那样:天下本没有路,走的人多了,就变成了路。在我看来,设计模式也一样,它并非是一种定律,而是前辈们总结下来的经验,我们学习并结合实际加以利用,而不是生搬硬套。

请不要以我为准!

请不要以我为准!

请不要以我为准!

定义

官腔:封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。

人话:但看定义很容易实现,并且让笔者第一时间联想到的策略模式,的确也能满足要求,但访问者更注重的是回调和递归。在元素中预留访问的入口,在访问者中实现元素的访问。若还是不明白,请看下面的应用场景。

应用场景

情景1

老样子,先假设一个非常简单的样例。为了尽量贴合所谓的定义。

假定,手里现在有一批教师和学生的数据,我们需要实现一下的要求:

  1. 查询出年龄超过50岁的教师。
  2. 查询出语数外,三门课总分最高的学生。
  3. 查询出平均分最高的班级。
1.定义元素对象
@Data
public class DvPerson {
    private String name;

    private int age;

    // 1老师 学生
    private int type;

    public DvPerson() {
    }

    public DvPerson(String name, int type) {
        this.name = name;
        this.type = type;
    }

    public DvPerson(String name, int age, int type) {
        this.name = name;
        this.age = age;
        this.type = type;
    }
}


@EqualsAndHashCode(callSuper = true)
@Data
public class DvStudent extends DvPerson{
    /**
     * 班级名称
     */
    private String ClassName;

    private double chineseScore;
    private double englishScore;
    private double mathScore;


    public DvStudent(String name,  String className, double chineseScore, double englishScore, double mathScore) {
        super(name, 2);
        ClassName = className;
        this.chineseScore = chineseScore;
        this.englishScore = englishScore;
        this.mathScore = mathScore;
    }

    public DvStudent() {
    }
}

@EqualsAndHashCode(callSuper = true)
@Data
public class DvTeacher extends DvPerson{

    public DvTeacher(String name) {
        super(name, 1);
    }

    public DvTeacher() {
    }

    public DvTeacher(String name, int age) {
        super(name, age, 1);
    }
}

复制代码

以上定义的教师和学生即所谓的元素,当然实际上需要的是一个元素的数组。

2.数据中心

接口定义

public interface IDataCenter {

    /**
     * 超过50岁的教师
     */
    List<DvTeacher> teacherGh_50();


    /**
     * 查询总分最高的学生
     * 不考虑可能分数会相同
     */
    DvPerson bestStudent();

    /**
     * 平均分最高的班级
     */
    String bestClassAvg();


    /**
     * 接受访问者
     *
     * @param visitor 访问者
     */
    <T>  void accept(IVisitor<T> visitor);
}
复制代码

实现类:

public class DataCenterImpl implements IDataCenter {
    private static List<DvPerson> personList = new ArrayList<>();

    private final static DataCenterImpl dataCenter = new DataCenterImpl();

    public DataCenterImpl addPerson(DvPerson person) {
        personList.add(person);
        return this;
    }

    public DataCenterImpl addPersons(DvPerson... person) {
        personList.addAll(Arrays.asList(person));
        return this;
    }

    public static DataCenterImpl getInstance() {
        return dataCenter;
    }

    @Override
    public List<DvTeacher> teacherGh_50() {
        return personList.stream().filter(p -> p.getType() == 1).map(p -> (DvTeacher) p).filter(p -> p.getAge() > 50).collect(Collectors.toList());
    }

    @Override
    public DvPerson bestStudent() {
        return personList.stream().filter(p -> p.getType() == 2).max((p1, p2) -> {
            DvStudent s1 = (DvStudent) p1;
            DvStudent s2 = (DvStudent) p2;
            double s1Score = s1.getChineseScore() + s1.getMathScore() + s1.getEnglishScore();
            double s2Score = s2.getChineseScore() + s2.getMathScore() + s2.getEnglishScore();
            return s1Score > s2Score ? 0 : -1;
        }).orElse(null);
    }

    @Override
    public String bestClassAvg() {
        Map<String, List<DvPerson>> classMap = personList.stream()
                .filter(p -> p.getType() == 2) //过滤出学生
                .collect(Collectors.groupingBy(p -> {
                    DvStudent s1 = (DvStudent) p;
                    return s1.getClassName();
                }, Collectors.toList()));//按班级分组

        List<DvClassBean> classBeans = classMap.entrySet().stream().map(k -> new DvClassBean(k.getKey(), k.getValue())).collect(Collectors.toList());
        return classBeans.stream().max((c1, c2) -> c1.getClassAvgScore() > c2.getClassAvgScore() ? 0 : -1).orElseGet(DvClassBean::new).getClassName();
    }

    @Override
    public <T> void accept(IVisitor<T> visitor) {
        visitor.doAction(getInstance());
    }

    static {
        System.out.println("------测试开始---");
        System.out.println("------初始化数据开始---");
        DvStudent sd = new DvStudent("张三", "一班", 60D, 66D, 90D);
        DvStudent sd1 = new DvStudent("李四", "二班", 66D, 99D, 89D);
        DvStudent sd2 = new DvStudent("王五", "二班", 67D, 55D, 99D);
        DvStudent sd3 = new DvStudent("小明", "三班", 80D, 44D, 100D);
        DvStudent sd4 = new DvStudent("小红", "一班", 80D, 56D, 79D);

        DvTeacher dt = new DvTeacher("张丽红老师", 40);
        DvTeacher dt2 = new DvTeacher("李国庆老师", 60);
        DvTeacher dt3 = new DvTeacher("王虎老师", 55);

        DataCenterImpl.getInstance().addPerson(sd).addPerson(sd1).addPerson(sd2).addPerson(sd3).addPerson(sd4).addPersons(dt, dt2, dt3);
        System.out.println("------初始化数据结束---");
    }

}
复制代码

说明一下,笔者没有在每个元素中去预留一个visit方法给访问者使用,而是将元素聚合在DataCenterImpl中,这里看实际情况,就目前的业务来说,笔者觉得在有限的业务情况内,这种聚合方式,可以尽量少的影响元素,但同样的会导致聚合类过于臃肿,若不喜欢这种方式,也可以对其进行修改。

DataCenterImpl中还使用到了单例模式,其内部初始化一个单例的DataCenterImpl对象,并在static代码块中完成数据的初始化。

另外也可以看到在accept方法内,并没有使用this,而是通过**getInstance()**再次获取了单例对象。若不使用static的变量,也可以改为this。

accept方法的泛型T,并没有实际意义,这里是笔者的一个习惯,因为在idea中,若泛型不指定则会提示警告。这种方式可以避免警告。笔者有强迫症,尽可能的消灭编译器提示的警告或异常,尽管不影响代码的运行。

还是那个观点,笔者进提供一种思路,不要以我为准。

3.定义访问者
public interface IVisitor<T> {

    T doAction();

    T doAction(IDataCenter center);
}
复制代码

这里假设访问者都比较简单,访问者都仅需访问一种数据结构。

三个访问者:

public class TeacherVisitor implements IVisitor<DvStudent> {

    @Override
    public DvStudent doAction() {
        //老师需要知道总分最高的学生
        return (DvStudent) DataCenterImpl.getInstance().bestStudent();
    }

    @Override
    public DvStudent doAction(IDataCenter center) {
        DvStudent student = (DvStudent) center.bestStudent();
        System.out.printf("总分最高的学生:%s%n", JSON.toJSONString(student));
        return student;
    }
}


public class PrincipalVisitor implements IVisitor<List<DvTeacher>> {

    @Override
    public List<DvTeacher> doAction() {
        //校长想知道年龄在50以上的老师
        return DataCenterImpl.getInstance().teacherGh_50();
    }

    @Override
    public List<DvTeacher> doAction(IDataCenter center) {
        List<DvTeacher> teacherList = center.teacherGh_50();
        System.out.printf("年龄超过50的教师:%s%n", JSON.toJSONString(teacherList));
        return teacherList;
    }
}


public class WebPageVisitor implements IVisitor<String>{
    @Override
    public String doAction() {
        //网页需要展示平均分最高的班级
        return DataCenterImpl.getInstance().bestClassAvg();
    }

    @Override
    public String doAction(IDataCenter center) {
        String classAvg = center.bestClassAvg();
        System.out.printf("平均分最高的班级:%s%n", classAvg);
        return classAvg;
    }
}
复制代码

代码可以看到,笔者在接口IVisitor中定义了两个方法。在无参的doAction方法中直接引用了数据中心DataCenterImpl的对应方法,在有参的doAction中是通过传入的数据中心IDataCenter进行处理。其中无参的方法调用也符合访问者的定义,但与实际访问者的应用不同,访问者强调回调或递归,而有参的方法更符合这一需求。

另外,笔者的并没有与网上其他的demo一样在一个访问者接口中实现多个不同维度元素的调用,若实际应用中有些数据可能并不需要,因此可以参考笔者的这种符合单一职责原则的接口设计,或设计一个通用的接口,若有需要,在扩展接口。例如:

public interface SimpleVisitor<T> extends IVisitor<T>{
    
    void doOtherAction();
}
复制代码

这种方式在spring框架在时分常见,虽然不想提所谓的原则,但在设计接口的时候,拒绝修改,支持扩展的方式更合适。不过这种方式就需要重载accept方法。

若需要接受不同的数据中心IDataCenter则也需要扩展IVisitor方法。

总之灵活应用,不要死记硬背。

4访问者控制中心(可选)

控制中心时基于无参的doAction方法来说的。

若想完全剥离访问者和被访问元素的关系,返回数据的结构都由数据中心控制,访问者仅负责调用,这样则需要一个控制中心。

仅提供一个概念作为参考,实际应用中,还要结合框架灵活应用,这里使用最简单的demo。

考虑两个问题:

  1. 如何知道应该初始化哪一个访问者?
  2. 如何进行调用?

笔者提供一种思路,将所有的访问者,统一存放,通过source字段进行区别,减弱业务与数据中心的依赖,不同的业务,若数据结构相同,可以共享一个source,这样要求提供的数据接口有一定的通用性。针对特殊的业务定制处理。

public class VisitorManager {
    private final static Map<String, IVisitor> VISITOR_MAP = new HashMap<>();

    static {
        VISITOR_MAP.put("teacher", new TeacherVisitor());
        VISITOR_MAP.put("web", new WebPageVisitor());
        VISITOR_MAP.put("principal", new PrincipalVisitor());
    }

    public Object doAction(String source) {
        IVisitor iVisitor = VISITOR_MAP.get(source);
        if (null == iVisitor) {
            throw new DemoException("不支持当前的异常");
        }
        return iVisitor.doAction();
    }
}
复制代码

这是非常非常简单的样板代码,实际开发中需要接口使用的框架调整。

5 测试

测试无参的doAction方法:

public class VisitorDemo {

    public static void main(String[] args) {
        WebPageVisitor pageVisitor = new WebPageVisitor();
        String maxAvg = pageVisitor.doAction();
        System.out.printf("平均分最高的班级:%s%n", maxAvg);

        TeacherVisitor teacherVisitor = new TeacherVisitor();
        DvStudent student = teacherVisitor.doAction();
        System.out.printf("总分最高的学生:%s%n", JSON.toJSONString(student));

        PrincipalVisitor principalVisitor = new PrincipalVisitor();
        List<DvTeacher> teachers = principalVisitor.doAction();
        System.out.printf("年龄超过50的教师:%s%n", JSON.toJSONString(teachers));
    }
}

复制代码

测试有参的doAction方法:


    public static void main(String[] args) {

        DataCenterImpl dataCenter = new DataCenterImpl();
        dataCenter.accept(new WebPageVisitor());
        dataCenter.accept(new PrincipalVisitor());
        dataCenter.accept(new TeacherVisitor());
    }
复制代码

UMl 图

老样子搬运菜鸟教程,感兴趣的可以去了解一下。demo比较简单,不过与笔者给的demo不一样,样例中ComputerPartVisitor 包含了对ComputerPart不同角度的观察,相当于一个聚合的观察对象。Computer 对象内聚合了ComputerPart的不同组件,同时拼装成为一个比较完整的电脑,并且内部可以循环执行不同组件的访问方法。如下:

public class Computer implements ComputerPart {
   
   ComputerPart[] parts;
 
   public Computer(){
      parts = new ComputerPart[] {new Mouse(), new Keyboard(), new Monitor()};      
   } 
 
 
   @Override
   public void accept(ComputerPartVisitor computerPartVisitor) {
      for (int i = 0; i < parts.length; i++) {
         parts[i].accept(computerPartVisitor);
      }
      computerPartVisitor.visit(this);
   }
}
复制代码

总体来说更符合访问者模式,但同样的,它要求访问者必须实现每一个组件的访问,且每个组件内部都需要预先为访问者提供入口,否则会运行错误。

image-20210601174016204

小结

初看概念的时候,只觉得和策略模式很相似。但实际应用中并不相同,访问者更关注回调和递归。在数据中心或元素内为访问者提供入口,可以接受任意的访问者,并且访问者内的方法入参又是元素或者数据中心,访问者内方法根据传入的元素或数据中心,调用预先写好的方法,相当于数据中心或元素自身执行了此方法。

在回到样例代码中,有参数的doAction方法更加灵活,访问者可以访问更多的实现了IDataCenter接口的数据,并且IDataCenter的accept()方法能接受不同种类的访问者。反观无参的doAction方法,与DataCenterImpl高度耦合,扩展性也不好。

若非要记一个概念,即:访问者与被访问对象之间是,你中有我,我中有你的关系。最终是在访问者内执行被访问对象提供的方法

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享