最近读到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. Java 的 clone 方法 && 浅复制和深复制

    1 Java中对象的创建过程 java创建对象的方式有以下两种: (1)使用new操作符创建一个对象 (2)使用clone的方法复制一个对象,(在Java中,clone是Object类的protect ...

  2. Ubuntu16.04 使用sudo cat EOF 编辑文件,提示Permission denied错误的解决办法

    一.执行命令报错 在Ubuntu16.04下,使用如下命令,修改hosts主机文件,居然提示权限错误: catty@node186:~$ sudo cat <<EOF > /etc/ ...

  3. pycharm上传代码到远程服务器

    本来不打算写了,可是,还是记不住 源自https://blog.csdn.net/zhangyu4863/article/details/80188207 我的是pycharm2018.1.4专业版: ...

  4. JVM垃圾回收(三)- GC算法:基础

    GC算法:基础 在介绍GC算法在实际场景中的实现之前,我们先定义一些必要的术语,以及GC算法的基本准则.具体的细节会因收集器的不同而稍有区别,但是基本上来说,所有的收集器会关注以下两个方面: 找出所有 ...

  5. LeetCode--028--实现strStr() (java)

    实现 strStr() 函数. 给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始).如果不存在,则返 ...

  6. 记录Sql2012附加Sql2008的数据库出错的解决方案

    只需要对要附加的数据文件[右键]->[属性]->[安全]->(选择“Authenticated Users”用户)[编辑]->让“Authenticated Users”用户具 ...

  7. 关于index.html被缓存问题

    关于web的缓存策略,推荐这篇文章:点击 在开发时候经常遇到一个问题,我们根据版本号去控制缓存问题,当我们发布新版本,使用心得版本号的时候,发现html里面引用的版本号却是旧的版本号 ,原来是该htm ...

  8. Confluence 6 升级完成后的检查

    这个页面中的文章将会为 Confluence 管理员为 Confluence 升级完成后 提供检查列表以确保 Confluence 的升级顺利完成.这个检查列表没有包含所有需要检查的错误,但是将会检查 ...

  9. Python —— 函数高级特性(切片、迭代、列表生成式、生成器、迭代器)

    一.切片(Slice) 在很多编程语言中,针对字符串提供了很多截取函数(i.e.  substring),目的就是对字符串切片.python中没有针对字符串的截取函数,需要通过“切片”来完成. 取一个 ...

  10. css过渡

    过渡transition是一个复合属性 例子1 .example{ width: 200px; height: 100px; background-color: blanchedalmond; tra ...