最近读到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);
}
public class AOODCourse extends Course {

    public AOODCourse(GregorianCalendar startDate, int students) {
super(startDate, students);
} public int getComputersInUse(GregorianCalendar date) {
return 0;
}
}
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;
}
}

代码实现了,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\n");
return line.toString();
} public GregorianCalendar getStartDate() {
return startDate;
} protected abstract String courseName();
protected abstract String computersInUse();
}
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";
}
}
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));
}
}
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());
}
}

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

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);
}
public class AOODCourse extends Course {

    public AOODCourse(GregorianCalendar startDate, int students) {
super(startDate, students);
} public int getComputersInUse(GregorianCalendar date) {
return 0;
}
}
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;
}
}
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\n");
} else if (course instanceof JavaCourse) {
report.append("Java ");
JavaCourse jc = (JavaCourse)course;
report.append(String.valueOf(jc.getTotalComputers())).append(" Computers\n");
}
}
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));
}
}

从上面的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);
}
public class CourseComputerReport extends CourseReport{

    protected void appendJavaLine(JavaCourse course, StringBuffer report) {
report.append("Java ");
report.append(String.valueOf(course.getTotalComputers())).append(" Computers\n");
} protected void appendAOODLine(AOODCourse course, StringBuffer report) {
report.append("AOOD 0 Computers\n");
} 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));
}
}

老鸟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);
public class AOODCourse extends Course {

    public AOODCourse(GregorianCalendar startDate, int students) {
super(startDate, students);
} @Override
void accept(CourseVisitor courseVisitor) {
courseVisitor.visit(this);
}
}
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;
}
}
public interface CourseVisitor {
void visit(AOODCourse aoodCourse);
void visit(JavaCourse javaCourse);
}
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\n");
} @Override
public void visit(JavaCourse javaCourse) {
report.append("Java ");
report.append(String.valueOf(javaCourse.getTotalComputers())).append(" Computers\n");
}
}
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);
}
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);
}
}

老鸟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);
}
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);
}
}
}
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;
}
}
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;
}
}
public interface CourseVisitor {
}
public interface AndroidCourseVisitor {
void visit(AndroidCourse androidCourse);
}
public interface AOODCourseVisitor {
void visit(AOODCourse aoodCourse);
}
public interface JavaCourseVisitor {
void visit(JavaCourse javaCourse);
}
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);
}
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);
}
}

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

Visitor 模式心得的更多相关文章

  1. 完成C++不能做到的事 - Visitor模式

    拿着刚磨好的热咖啡,我坐在了显示器前.“美好的一天又开始了”,我想. 昨晚做完了一个非常困难的任务并送给美国同事Review,因此今天只需要根据他们提出的意见适当修改代码并提交,一周的任务就完成了.剩 ...

  2. Visitor模式,Decorator模式,Extension Object模式

    Modem结构 Visitor模式 对于被访问(Modem)层次结构中的每一个派生类,访问者(Visitor)层次中都有一个对应的方法. 从派生类到方法的90度旋转. 新增类似的Windows配置函数 ...

  3. 设计模式之visitor模式,人人能懂的有趣实例

    设计模式,现在在网上随便搜都一大堆,为什么我还要写"设计模式"的章节呢? 两个原因: 1.本人觉得这是一个有趣的设计模式使用实例,所以记下来: 2.看着设计模式很牛逼,却不知道怎么 ...

  4. 设计模式:基于线程池的并发Visitor模式

    1.前言 第二篇设计模式的文章我们谈谈Visitor模式. 当然,不是简单的列个的demo,我们以电商网站中的购物车功能为背景,使用线程池实现并发的Visitor模式,并聊聊其中的几个关键点. 一,基 ...

  5. Java 的双重分发与 Visitor 模式

    双重分发(Double Dispatch) 什么是双重分发? 谈起面向对象的程序设计时,常说起的面向对象的「多态」,其中关于多态,经常有一个说法是「父类引用指向子类对象」. 这种父类的引用指向子类对象 ...

  6. 【转载】完成C++不能做到的事 - Visitor模式

    原文: 完成C++不能做到的事 - Visitor模式 拿着刚磨好的热咖啡,我坐在了显示器前.“美好的一天又开始了”,我想. 昨晚做完了一个非常困难的任务并送给美国同事Review,因此今天只需要根据 ...

  7. 设计模式之——visitor模式

    visitor模式,又叫访问者模式,把结构和数据分开,编写一个访问者,去访问数据结构中的元素,然后把对各元素的处理全部交给访问者类.这样,当需要增加新的处理时候,只需要编写新的 访问者类,让数据结构可 ...

  8. Visitor模式(访问者设计模式)

    Visitor ? 在Visitor模式中,数据结构与处理被分离开来.我们编写一个表示"访问者"的类来访问数据结构中的元素, 并把对各元素的处理交给访问者类.这样,当需要增加新的处 ...

  9. Behavioral模式之Visitor模式

    1.意图 表示一个作用于某对象结构中的各元素的操作.它使你能够在不改变各元素的类的前提下定义作用于这些元素的新操作. 2.别名 无 3.动机 考虑一个编译器.他将源程序表示为一个抽象语法树.该编译器须 ...

随机推荐

  1. PHP curl Post请求和Get请求~

    //获取的参数 $api_key = '8a82d53a57b06c1d835d129f7e43d49c'; $orderNum = pdo_fetch('select ddlm_order_no f ...

  2. RN截图并且下载问题

    神奇的BUG一大堆. 需求-->截图并且下载图片 实现: import ViewShot from "react-native-view-shot"; CameraRoll ...

  3. blob canvas img dataUrl的互相转换和用处

    blob:代表了一段二进制数据 初始化:var blob = new Blob(array,option)//其中array里面可以包含任意类型对象,option指数据类型如array是['<h ...

  4. diango admin 添加成员报错

    [报错内容]: IntegrityError at /admin/users/userprofile/add/ (1452, 'Cannot add or update a child row: a ...

  5. 【A tour of go】练习题

    练习:循环与函数 (1)题目 为了练习函数与循环,我们来实现一个平方根函数:用牛顿法实现平方根函数. 计算机通常使用循环来计算 x 的平方根.从某个猜测的值 z 开始,我们可以根据 z² 与 x 的近 ...

  6. 『高性能模型』卷积复杂度以及Inception系列

    转载自知乎:卷积神经网络的复杂度分析 之前的Inception学习博客: 『TensorFlow』读书笔记_Inception_V3_上 『TensorFlow』读书笔记_Inception_V3_下 ...

  7. Java 使用jxl对Excel进行操作

    一个作业需要对excel数据进行离散化,想起好像可以用java对excel数据进行处理,因此学习使用, 在网上也有很多人对这个内容解释,但是还是觉得有些杂,就自己整理了一些别人写的内容. /***** ...

  8. MySQL常用语法命令及函数

    #创建数据库# create database 数据库名; #查看数据库# show databases; #选择数据库# use 数据库名; #删除数据库# drop database 数据库名; ...

  9. 3.4 自动测试初步《精通ASP.NET MVC 5》

    概述 ASP.NET MVC 框架已被设计成易于建立自动测试,并易于采用诸如测试驱动开发(TDD)等的开发方法学.ASP.NET MVC 为自动化测试提供了一个理想平台. 从广义上讲,当今的 Web ...

  10. 【Linux】gdb调试

    g++ -g ... gdb l    列出代码,回车键继续 break main / 行号 加断点 n    单步运行 s    单步运行(可进入函数) p    输出变量 p *array@len ...