• 设计模式学习笔记(二十一)访问者模式及其实现


    访问者模式(Visitor Pattern)指将作用域某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作。借用《Java设计模式》中的例子说明:在医院医生开具药单后,划价人员拿到药单后会根据药单上的药品名称和数量计算总价,而药房工作人员则根据药品名称和数量准备药品。如下图所示:

    image-20220411081209135

    那么药品处方可以看成是一个药品信息的集合,里面包含了一种或多种不同类型的药品信息,不同类型的工作人员在操作统一药品信息集合时将提供不同的处理方式,而且可能还会增加新类型的工作人员来操作处方单。这就是访问者模式的典型应用场景。

    一、访问者模式介绍

    1.1 访问者模式的结构

    访问者模式是一种较为复杂的行为型模式,它包含访问者(Visitor)和被访问元素(Element)两个主要组成部分。下面就来看看访问者模式的具体结构:

    image-20220411095609773

    • Visitor:抽象访问者,为对象结构中的每个具体元素类声明一个访问操作
    • ConcreteVisitor1、ConcreteVisitor2:具体访问者,实现抽象访问者声明的操作
    • Element:抽象元素,定义一个accept()方法
    • ConcreteElement1、ConcreteElement2:具体元素,实现抽象元素中的accept()方法,在accept()方法中调用访问者的访问方法以便完成对一个元素的操作。
    • ObjectStructure:对象结构,它是一个元素的集合,用于存放元素对象,并且提供了遍历其内部元素的方法。
    • Client:客户端

    1.2 访问者模式的实现

    根据上面的类图,首先是抽象访问者,为每一种具体类型对象都会提供一个访问方法:

    public interface Visitor {
    
        void visit(ConcreteElementA elementA);
        void visit(ConcreteElementB elementB);
    }
    

    接下来是具体访问者,实现抽象访问者的声明方法

    public class ConcreteVisitor1 implements Visitor{
    
        @Override
        public void visit(ConcreteElementA elementA) {
            System.out.println("ConcreteVisitor1 访问 ConcreteElementA: " + elementA.operationA());
        }
    
        @Override
        public void visit(ConcreteElementB elementB) {
            System.out.println("ConcreteVisitor1 访问 ConcreteElementB: " + elementB.operationB());
        }
    }
    public class ConcreteVisitor2 implements Visitor {
    
        @Override
        public void visit(ConcreteElementA elementA) {
            System.out.println("ConcreteVisitor2 访问 ConcreteElementA: " + elementA.operationA());
        }
    
        @Override
        public void visit(ConcreteElementB elementB) {
            System.out.println("ConcreteVisitor2 访问 ConcreteElementB: " + elementB.operationB());
        }
    }
    

    然后是抽象元素接口,定义一个accept()方法,用于接受访问者的访问:

    public interface Element {
    
        void accept(Visitor visitor);
    }
    

    下面是实现抽象元素接口的具体元素类,除了重载accept()方法,还实现对应的具体操作方法

    public class ConcreteElementA implements Element{
    
        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this);
        }
    
        public String operationA() {
            return "ConcreteElementA的操作方法";
        }
    }
    public class ConcreteElementB implements Element{
    
        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this);
        }
    
        public String operationB() {
            return "ConcreteElementB的操作方法";
        }
    }
    

    最后是对象结构类,是一个对元素进行操作的容器,提供访问者遍历容器中所有元素的方法

    public class ObjectStructure {
    
        private List<Element> elementList = new ArrayList<>();
    
        public void accept(Visitor visitor) {
            Iterator<Element> it = elementList.iterator();
            while(it.hasNext()) {
                it.next().accept(visitor);
            }
        }
    
        public void add(Element element) {
            elementList.add(element);
        }
    
        public void remove(Element element) {
            elementList.remove(element);
        }
    }
    

    客户端测试类:

    public class Client {
        public static void main(String[] args) {
            //将具体元素注入对象结构中
            ObjectStructure objectStructure = new ObjectStructure();
            objectStructure.add(new ConcreteElementA());
            objectStructure.add(new ConcreteElementB());
            //具体访问者访问具体元素
            objectStructure.accept(new ConcreteVisitor1());
            objectStructure.accept(new ConcreteVisitor2());
    
        }
    }
    

    测试结果:

    ConcreteVisitor1 访问 ConcreteElementA: ConcreteElementA的操作方法
    ConcreteVisitor1 访问 ConcreteElementB: ConcreteElementB的操作方法
    ConcreteVisitor2 访问 ConcreteElementA: ConcreteElementA的操作方法
    ConcreteVisitor2 访问 ConcreteElementB: ConcreteElementB的操作方法
    

    二、访问者模式的应用场景

    在下面的情况可以考虑使用访问者模式:

    • 一个对象结构中包含多个类型的对象,希望对这些对象实施一些依赖其具体类型的操作
    • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作
    • 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作

    三、访问者模式实战

    本案例模拟学校中学生和老师对于不同用户的访问视角(案例来源于《重学Java设计模式》)

    这个案例场景我们模拟校园中有学⽣和⽼师两种身份的⽤户,那么对于家⻓和校⻓关⼼的⻆度来看,他 们的视⻆是不同的。家⻓更关⼼孩⼦的成绩和⽼师的能⼒,校⻓更关⼼⽼师所在班级学⽣的⼈数和升学 率

    从前面第一节的结构图和实现代码就可以知道,访问者模式的整体类结构相对复杂,下面就来看看该案例的核心逻辑实现:

    1. 需要建立用户抽象类和抽象访问方法,再由不同的用户实现(相当于前面的元素),这里的用户指看老师和学生;
    2. 建立访问者接口,用于不同人员的访问操作,这里的访问者指校长和家长;
    3. 最终是对数据的看板建设,用于实现不同视角的访问结果输出(相当于前面的对象结构);

    具体代码实现

    1. 用户抽象类及具体实现类

    先来看看用户抽象类,类似于第一节中的抽象元素类

    public abstract class User {
    
        /**姓名*/
        public String name;
        /**用户的身份,包括学生和教师*/
        public String identity;
        /**所属班级*/
        public String clazz;
    
        public User(String name, String identity, String clazz) {
            this.name = name;
            this.identity = identity;
            this.clazz = clazz;
        }
    
        public abstract void accept(Visitor visitor);
    }
    

    具体用户类,包括学生和老师,每个具体用户可以实现对应不同的方法,比如学生的排名,老师的升学率方法。

    public class Student extends User{
    
        public Student(String name, String identity, String clazz) {
            super(name, identity, clazz);
        }
    
        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this);
        }
    
        public int ranking() {
            return (int) (Math.random()*100);
        }
    }
    public class Teacher extends User {
    
        public Teacher(String name, String identity, String clazz) {
            super(name, identity, clazz);
        }
    
        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this);
        }
        //升学率
        public double entranceRatio() {
            return BigDecimal.valueOf(Math.random() * 100).setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
        }
    
    }
    
    1. 抽象访问者及具体实现类

    先看看抽象访问者接口:

    public interface Visitor {
    
        void visit(Student student);
    
        void visit(Teacher teacher);
    }
    

    对应的具体访问者实现,包括父母和校长类。不同访问者的访问角度也不相同,校长看重教师的升学率,父母看重学生的排名

    public class Parent implements Visitor{
    
        private Logger logger = LoggerFactory.getLogger(Parent.class);
    
        @Override
        public void visit(Student student) {
            logger.info("学生信息 姓名:{} 班级:{} 排名:{}", student.name, student.clazz, student.ranking());
        }
    
        @Override
        public void visit(Teacher teacher) {
            logger.info("老师信息 姓名:{} 班级:{}", teacher.name, teacher.clazz);
        }
    }
    public class Principal implements Visitor{
    
        private Logger logger = LoggerFactory.getLogger(Principal.class);
    
        @Override
        public void visit(Student student) {
            logger.info("学生信息 姓名: {} 班级:{}", student.name, student.clazz);
        }
    
        @Override
        public void visit(Teacher teacher) {
            logger.info("老师信息 姓名: {} 班级:{} 升学率:{}", teacher.name, teacher.clazz, teacher.entranceRatio());
        }
    }
    
    1. 数据看板

    数据看板就类似于第一节中的对象结构(ObjectStructure),初始化具体用户的信息,展示访问者的访问角度信息:

    public class DataView {
    
        List<User> userList = new ArrayList<>();
    
        public DataView() {
            userList.add(new Student("Ethan", "普通班", "高一1班"));
            userList.add(new Student("Tom", "重点班", "高一2班"));
            userList.add(new Student("Peter", "重点班", "高一3班"));
            userList.add(new Teacher("张三", "普通班", "高一1班"));
            userList.add(new Teacher("李四", "重点班", "高一2班"));
            userList.add(new Teacher("王五", "重点班", "高一3班"));
        }
    
        public void show(Visitor visitor) {
            for (User user : userList) {
                user.accept(visitor);
            }
        }
    }
    
    1. 测试类

    对整个流程进行测试:

    public class ApiTest {
    
        private Logger logger = LoggerFactory.getLogger(ApiTest.class);
    
        @Test
        public void test() {
            DataView dataView = new DataView();
    
            logger.info("家长视角访问:");
            dataView.show(new Parent());
    
            logger.info("校长视角访问:");
            dataView.show(new Principal());
        }
    }
    

    结果为:

    12:27:18.983 [main] INFO  ApiTest - 家长视角访问:
    12:27:18.983 [main] INFO  visitor.Parent - 学生信息 姓名:Ethan 班级:高一1班 排名:22
    12:27:18.983 [main] INFO  visitor.Parent - 学生信息 姓名:Tom 班级:高一2班 排名:2
    12:27:18.983 [main] INFO  visitor.Parent - 学生信息 姓名:Peter 班级:高一3班 排名:0
    12:27:18.983 [main] INFO  visitor.Parent - 老师信息 姓名:张三 班级:高一1班
    12:27:18.983 [main] INFO  visitor.Parent - 老师信息 姓名:李四 班级:高一2班
    12:27:18.983 [main] INFO  visitor.Parent - 老师信息 姓名:王五 班级:高一3班
    12:27:18.983 [main] INFO  ApiTest - 校长视角访问:
    12:27:18.983 [main] INFO  visitor.Principal - 学生信息 姓名: Ethan 班级:高一1班
    12:27:18.983 [main] INFO  visitor.Principal - 学生信息 姓名: Tom 班级:高一2班
    12:27:18.983 [main] INFO  visitor.Principal - 学生信息 姓名: Peter 班级:高一3班
    12:27:18.998 [main] INFO  visitor.Principal - 老师信息 姓名: 张三 班级:高一1班 升学率:39.85
    12:27:18.998 [main] INFO  visitor.Principal - 老师信息 姓名: 李四 班级:高一2班 升学率:88.14
    12:27:18.998 [main] INFO  visitor.Principal - 老师信息 姓名: 王五 班级:高一3班 升学率:44.65
    

    参考资料

    《Java设计模式》

    《重学Java设计模式》

    http://c.biancheng.net/view/1397.html

  • 相关阅读:
    大数据技术之_04_Hadoop学习_02_HDFS_DataNode(面试开发重点)+HDFS 2.X新特性
    大数据技术之_04_Hadoop学习_01_HDFS_HDFS概述+HDFS的Shell操作(开发重点)+HDFS客户端操作(开发重点)+HDFS的数据流(面试重点)+NameNode和SecondaryNameNode(面试开发重点)
    请不要重复犯我在学习Python和Linux系统上的错误
    教你如何在Kali Linux 环境下设置蜜罐?
    Docker 基础技术:Linux Namespace(下)
    真有用?Snap和Flatpak 通吃所有发行版的打包方式。
    安装Fedora 24后必要的设置
    爆料喽!!!开源日志库Logger的剖析分析
    简单易懂的crontab设置工具集
    新手必看,老鸟绕道–LAMP简易安装
  • 原文地址:https://www.cnblogs.com/EthanWong/p/16129687.html
Copyright © 2020-2023  润新知