• Visitor 模式心得


    最近读到Visitor模式,还是一知半解的。偶然翻到Uncle Bob对该模式的推导过程,有所心得,和大家分享一下。 Uncle Bob 的链接是: http://butunclebob.com/ArticleS.UncleBob.VisitorVersusInstanceOf。个人觉得该模式用来操作复杂对象集合,特别适用于报表生成。因为报表的来源相对稳定(复杂数据集合),但是表现形式却是千变万化。言归正传,我将该博客的内容按照自己的理解分享出来,如果有什么不对的地方,请指正。

    首先有一个如下简单的场景,Bob先生的公司提供计算机方面的培训服务,对社会人士开设二门课程,一门是OOD, 一门是Java,java课程需要上机(结对编程,所以二个人用一台机器),所以需要根据报名的时间计算具体的机器数目:

    public abstract class Course {
    
        protected GregorianCalendar startDate;
        protected int students;
    
        public Course(GregorianCalendar startDate, int students) {
            this.startDate = (GregorianCalendar) startDate.clone();
            this.students = students;
        }
    
        abstract public int getComputersInUse(GregorianCalendar date);
    }
    View Code
    public class AOODCourse extends Course {
    
        public AOODCourse(GregorianCalendar startDate, int students) {
            super(startDate, students);
        }
    
        public int getComputersInUse(GregorianCalendar date) {
            return 0;
        }
    }
    View Code
    public class JavaCourse extends Course {
    
        private GregorianCalendar endDate;
    
        public JavaCourse(GregorianCalendar startDate, int students) {
            super(startDate, students);
            endDate = (GregorianCalendar) startDate.clone();
            endDate.add(Calendar.DAY_OF_WEEK, 4);
        }
    
        public int getComputersInUse(GregorianCalendar date) {
            int resources = 0;
            if (!date.before(startDate) && !date.after(endDate)) {
                resources = Math.round(students/2);
            }
            return resources;
        }
    }
    View Code

    代码实现了,B先生很happy。不久客户说需要生一份报表,B先生想了5分钟,洋洋洒洒写下如下代码:

    public abstract class Course {
    
        protected GregorianCalendar startDate;
        protected int students;
        protected SimpleDateFormat dateFormat;
    
        public Course(GregorianCalendar startDate, int students) {
            this.startDate = (GregorianCalendar) startDate.clone();
            this.students = students;
            this.dateFormat = new SimpleDateFormat("MM/dd/yyyy");
        }
    
        public String generateComputerReportLine() {
            StringBuffer line = new StringBuffer();
            line.append(dateFormat.format(startDate.getTime())).append(" ")
                    .append(courseName()).append(" ")
                    .append(computersInUse()).append(" Computers
    ");
            return line.toString();
        }
    
        public GregorianCalendar getStartDate() {
            return startDate;
        }
    
        protected abstract String courseName();
        protected abstract String computersInUse();
    }
    View Code
    public class AOODCourse extends Course {
    
        public AOODCourse(GregorianCalendar startDate, int students) {
            super(startDate, students);
        }
    
        @Override
        protected String courseName() {
            return "AOOD";
        }
    
        @Override
        protected String computersInUse() {
            return "0";
        }
    }
    View Code
    public class JavaCourse extends Course {
    
        private GregorianCalendar endDate;
    
        public JavaCourse(GregorianCalendar startDate, int students) {
            super(startDate, students);
            endDate = (GregorianCalendar) startDate.clone();
            endDate.add(Calendar.DAY_OF_WEEK, 4);
        }
    
        @Override
        protected String courseName() {
            return "Java";
        }
    
        @Override
        protected String computersInUse() {
            return String.valueOf(Math.round(students/2));
        }
    }
    View Code
    public class CourseResourceTracker<T extends Course> {
    
        Set<T> courses = new HashSet<>();
    
        public String generateReport() {
            StringBuilder reports = new StringBuilder();
            for (Iterator i = courses.iterator(); i.hasNext();) {
                T course = (T) i.next();
                reports.append(course.generateComputerReportLine()) ;
            }
            return reports.toString();
        }
    
        public void add(T course) {
            if (course == null) return;
    
            courses.add(course);
        }
    
        public void remove(T course) {
            if (course == null) return;
    
            courses.remove(course);
        }
    
        public static void main(String[] param) {
            CourseResourceTracker tracker = new CourseResourceTracker();
            tracker.add(new AOODCourse(new GregorianCalendar(2015, 3, 20), 10));
            tracker.add(new JavaCourse(new GregorianCalendar(2015, 3, 20), 10));
            System.out.println("Report is : " + tracker.generateReport());
        }
    }
    View Code

    报表很简单, 打印结果如下, 格式为: 时间 + 名称 + 数量

    Report is : 04/20/2015 AOOD 0 Computers
    04/20/2015 Java 5 Computers

    B洋洋得意,客户又来了新的需求,前面的报表太简单了,需要针对不同的课程打印不同的内容:

    public abstract class Course {
    
        protected GregorianCalendar startDate;
        protected int students;
        protected SimpleDateFormat dateFormat;
    
        public Course(GregorianCalendar startDate, int students) {
            this.startDate = (GregorianCalendar) startDate.clone();
            this.students = students;
            this.dateFormat = new SimpleDateFormat("MM/dd/yyyy");
        }
    
        public GregorianCalendar getStartDate() {
            return startDate;
        }
    
        abstract public int getComputersInUse(GregorianCalendar date);
    }
    View Code
    public class AOODCourse extends Course {
    
        public AOODCourse(GregorianCalendar startDate, int students) {
            super(startDate, students);
        }
    
        public int getComputersInUse(GregorianCalendar date) {
            return 0;
        }
    }
    View Code
    public class JavaCourse extends Course {
    
        private GregorianCalendar endDate;
    
        public JavaCourse(GregorianCalendar startDate, int students) {
            super(startDate, students);
            endDate = (GregorianCalendar) startDate.clone();
            endDate.add(Calendar.DAY_OF_WEEK, 4);
        }
    
        public int getComputersInUse(GregorianCalendar date) {
            int resources = 0;
            if (!date.before(startDate) && !date.after(endDate)) {
                resources = Math.round(students/2);
            }
            return resources;
        }
    
        public int getTotalComputers() {
            return students/2;
        }
    }
    View Code
    public class CourseComputerReport {
        protected SimpleDateFormat dateFormat;
    
        public CourseComputerReport() {
            dateFormat = new SimpleDateFormat("MM/dd/yyyy");
        }
    
        public String generateComputerReport(List courses) {
            if (courses == null || courses.size() == 0) return null;
    
            StringBuffer report = new StringBuffer();
            for (Iterator i = courses.iterator(); i.hasNext();) {
                Course course = (Course) i.next();
                report.append(dateFormat.format(course.getStartDate().getTime())).append(" ");
                if (course instanceof AOODCourse) {
                    report.append("AOOD 0 Computers
    ");
                } else if (course instanceof JavaCourse) {
                    report.append("Java ");
                    JavaCourse jc = (JavaCourse)course;
                    report.append(String.valueOf(jc.getTotalComputers())).append(" Computers
    ");
                }
            }
            return report.toString();
        }
    
        public static void main(String[] param) {
            CourseComputerReport report = new CourseComputerReport();
            List<Course> courses = new ArrayList<>();
            courses.add(new AOODCourse(new GregorianCalendar(2015, 3, 20), 10));
            courses.add(new JavaCourse(new GregorianCalendar(2015, 3, 20), 10));
            System.out.println("Report is : " + report.generateComputerReport(courses));
        }
    }
    View Code

    从上面的UML 类图可以看出,CourseComputerReport 因为对每门课程的format不一致,需要遍历的时候针对不同的课程设置不同的格式从而导致CourseReport和Course之前出现强耦合, 可以通过重构方法 generateComputerReport 使之更加合理:

    public abstract class CourseReport {
        protected SimpleDateFormat dateFormat;
    
        public CourseReport() {
            dateFormat = new SimpleDateFormat("MM/dd/yyyy");
        }
    
        public String generateComputerReport(List courses) {
            StringBuffer report = new StringBuffer();
            for (Iterator i = courses.iterator(); i.hasNext();) {
                Course course = (Course) i.next();
                report.append(dateFormat.format(course.getStartDate().getTime())).append(" ");
                if (course instanceof AOODCourse) {
                    appendAOODLine((AOODCourse)course, report);
                } else if (course instanceof JavaCourse) {
                    appendJavaLine((JavaCourse) course, report);
                }
            }
            return report.toString();
        }
    
        protected abstract void appendJavaLine(JavaCourse course, StringBuffer report);
        protected abstract void appendAOODLine(AOODCourse course, StringBuffer report);
    }
    View Code
    public class CourseComputerReport extends CourseReport{
    
        protected void appendJavaLine(JavaCourse course, StringBuffer report) {
            report.append("Java ");
            report.append(String.valueOf(course.getTotalComputers())).append(" Computers
    ");
        }
    
        protected void appendAOODLine(AOODCourse course, StringBuffer report) {
            report.append("AOOD 0 Computers
    ");
        }
    
        public static void main(String[] param) {
            CourseComputerReport report = new CourseComputerReport();
            List<Course> courses = new ArrayList<>();
            courses.add(new AOODCourse(new GregorianCalendar(2015, 3, 20), 10));
            courses.add(new JavaCourse(new GregorianCalendar(2015, 3, 20), 10));
            System.out.println("Report is : " + report.generateComputerReport(courses));
        }
    }
    View Code

    老鸟L看了B的实现后,提出了自己的看法,为什么 CourseReport 需要依赖具体的Course, 能否将Course告诉reporter 而不是Reporter来判断具体的Course。 并将Visitor的经典类图随手抛给小B:

    小B恍然大悟,修改代码如下:

    public abstract class Course {
    
        protected GregorianCalendar startDate;
        protected int students;
        protected SimpleDateFormat dateFormat;
    
        public Course(GregorianCalendar startDate, int students) {
            this.startDate = (GregorianCalendar) startDate.clone();
            this.students = students;
            this.dateFormat = new SimpleDateFormat("MM/dd/yyyy");
        }
    
        public GregorianCalendar getStartDate() {
            return startDate;
        }
    
        abstract void accept(CourseVisitor courseVisitor);
    View Code
    public class AOODCourse extends Course {
    
        public AOODCourse(GregorianCalendar startDate, int students) {
            super(startDate, students);
        }
    
        @Override
        void accept(CourseVisitor courseVisitor) {
            courseVisitor.visit(this);
        }
    }
    View Code
    public class JavaCourse extends Course {
    
        private GregorianCalendar endDate;
    
        public JavaCourse(GregorianCalendar startDate, int students) {
            super(startDate, students);
            endDate = (GregorianCalendar) startDate.clone();
            endDate.add(Calendar.DAY_OF_WEEK, 4);
        }
    
        @Override
        void accept(CourseVisitor courseVisitor) {
            courseVisitor.visit(this);
        }
    
        public int getTotalComputers() {
            return students/2;
        }
    }
    View Code
    public interface CourseVisitor {
        void visit(AOODCourse aoodCourse);
        void visit(JavaCourse javaCourse);
    }
    View Code
    public class ReportCourseVisitor implements CourseVisitor {
    
        protected StringBuffer report;
    
        public ReportCourseVisitor(StringBuffer report) {
            this.report = report;
    
        }
    
        @Override
        public void visit(AOODCourse aoodCourse) {
            report.append("AOOD 0 Computers
    ");
        }
    
        @Override
        public void visit(JavaCourse javaCourse) {
            report.append("Java ");
            report.append(String.valueOf(javaCourse.getTotalComputers())).append(" Computers
    ");
        }
    }
    View Code
    public abstract class CourseReport {
    
        protected SimpleDateFormat dateFormat;
    
        public CourseReport() {
            dateFormat = new SimpleDateFormat("MM/dd/yyyy");
        }
    
        public String generateComputerReport(List courses) {
            StringBuffer report = new StringBuffer();
            CourseVisitor v = makeReportVisitor(report);
            for (Iterator i = courses.iterator(); i.hasNext();) {
                Course course = (Course) i.next();
                report.append(dateFormat.format(course.getStartDate().getTime())).append(" ");
                course.accept(v);
            }
            return report.toString();
        }
    
        protected abstract CourseVisitor makeReportVisitor(StringBuffer report);
    }
    View Code
    public class CourseComputerReport extends CourseReport{
    
        public static void main(String[] param) {
            CourseComputerReport report = new CourseComputerReport();
            List<Course> courses = new ArrayList<>();
            courses.add(new AOODCourse(new GregorianCalendar(2015, 3, 20), 10));
            courses.add(new JavaCourse(new GregorianCalendar(2015, 3, 20), 10));
            System.out.println("Report is : " + report.generateComputerReport(courses));
        }
    
        @Override
        protected CourseVisitor makeReportVisitor(StringBuffer report) {
            return new ReportCourseVisitor(report);
        }
    }
    View Code

     老鸟L看了以后表示满意,同时指出如果添加新的Course,由于所有的course都依赖CourseVisitor,而courseVisitor需要添加新的接口,所有的course需要重新打包和编译。是否可以新建一个空接口CourseVisitor 使Course依赖这个接口从而达到隔离变化的目的。需要修改的是需要将courseVisitor Cast到具体的实现类,主要的代码实现是:

     @Override
        void accept(CourseVisitor courseVisitor) {
            if (courseVisitor instanceof AOODCourseVisitor) {
                ((AOODCourseVisitor) courseVisitor).visit(this);
            }
        }

    小B撸撸袖子,大笔一挥,修改代码如下:

    public abstract class Course {
    
        protected GregorianCalendar startDate;
        protected int students;
        protected SimpleDateFormat dateFormat;
    
        public Course(GregorianCalendar startDate, int students) {
            this.startDate = (GregorianCalendar) startDate.clone();
            this.students = students;
            this.dateFormat = new SimpleDateFormat("MM/dd/yyyy");
        }
    
        public GregorianCalendar getStartDate() {
            return startDate;
        }
    
        abstract void accept(CourseVisitor courseVisitor);
    }
    View Code
    public class AOODCourse extends Course {
    
        public AOODCourse(GregorianCalendar startDate, int students) {
            super(startDate, students);
        }
    
        @Override
        void accept(CourseVisitor courseVisitor) {
            if (courseVisitor instanceof AOODCourseVisitor) {
                ((AOODCourseVisitor) courseVisitor).visit(this);
            }
        }
    }
    View Code
    public class JavaCourse extends Course {
    
        private GregorianCalendar endDate;
    
        public JavaCourse(GregorianCalendar startDate, int students) {
            super(startDate, students);
            endDate = (GregorianCalendar) startDate.clone();
            endDate.add(Calendar.DAY_OF_WEEK, 4);
        }
    
        @Override
        void accept(CourseVisitor courseVisitor) {
            if (courseVisitor instanceof JavaCourseVisitor) {
                ((JavaCourseVisitor) courseVisitor).visit(this);
            }
        }
    
        public int getTotalComputers() {
            return students/2;
        }
    }
    View Code
    public class AndroidCourse extends Course {
    
        private GregorianCalendar endDate;
    
        public AndroidCourse(GregorianCalendar startDate, int students) {
            super(startDate, students);
            endDate = (GregorianCalendar) startDate.clone();
            endDate.add(Calendar.DAY_OF_WEEK, 4);
        }
    
        @Override
        void accept(CourseVisitor courseVisitor) {
            if (courseVisitor instanceof AndroidCourseVisitor) {
                ((AndroidCourseVisitor) courseVisitor).visit(this);
            }
        }
    
        public int getTotalComputers() {
            return students/2;
        }
    }
    View Code
    public interface CourseVisitor {
    }
    View Code
    public interface AndroidCourseVisitor {
        void visit(AndroidCourse androidCourse);
    }
    View Code
    public interface AOODCourseVisitor {
        void visit(AOODCourse aoodCourse);
    }
    View Code
    public interface JavaCourseVisitor {
        void visit(JavaCourse javaCourse);
    }
    View Code
    public abstract class CourseReport {
    
        protected SimpleDateFormat dateFormat;
    
        public CourseReport() {
            dateFormat = new SimpleDateFormat("MM/dd/yyyy");
        }
    
        public String generateComputerReport(List courses) {
            StringBuffer report = new StringBuffer();
            CourseVisitor v = makeCourseVisitor(report);
            for (Iterator i = courses.iterator(); i.hasNext();) {
                Course course = (Course) i.next();
                report.append(dateFormat.format(course.getStartDate().getTime())).append(" ");
                course.accept(v);
            }
            return report.toString();
        }
    
        protected abstract CourseVisitor makeCourseVisitor(StringBuffer report);
    }
    View Code
    public class CourseComputerReport extends CourseReport {
    
        public static void main(String[] param) {
            CourseComputerReport report = new CourseComputerReport();
            List<Course> courses = new ArrayList<>();
            courses.add(new AOODCourse(new GregorianCalendar(2015, 3, 20), 10));
            courses.add(new JavaCourse(new GregorianCalendar(2015, 3, 20), 10));
            courses.add(new AndroidCourse(new GregorianCalendar(2015, 3, 20), 10));
            System.out.println("Report is : " + report.generateComputerReport(courses));
        }
    
        @Override
        protected CourseVisitor makeCourseVisitor(StringBuffer report) {
            return new ReportCourseVisitor(report);
        }
    }
    View Code

     顺便说一下,上面使用的模式名称是Acyclic visitor, 是为了解决添加新的item 导致所有的item需要重新编译和打包的问题。

  • 相关阅读:
    学习java随笔第五篇:流程控制
    学习java随笔第四篇:运算符
    学习java随笔第三篇:java的基本数据类型
    知识精简
    性能优化(详细)
    2,8,10,16进制转换
    前端优化35例
    性能优化
    字面量自定义事件

  • 原文地址:https://www.cnblogs.com/budoudou/p/6921471.html
Copyright © 2020-2023  润新知