最近读到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. 承接微信小程序外包 H5外包就找北京动点软件开发团队

    承接小程序外包 微信小程序外包 H5外包 就找北京动点软件 长年承接微信小程序.微信公众号开发 全职的H5开发团队,开发过几十款微信小程序公众号案例 欢迎来电咨询,索取案例! QQ:372900288 ...

  2. Hybrid App 开发模式

    开发移动App主要有三种模式:Native. Hybrid 和 Web App. 需要注意的一点是在选择开发模式的时候,要根据你的项目类型(图片类?视频类?新闻类?等),产品业务和人员技术储备等做权衡 ...

  3. hdu 4856 Tunnels 状态压缩dp

    Tunnels Time Limit: 3000/1500 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Problem ...

  4. radio为什么不能选择。急急急

    <div class="control-group"> <label class="control-label" for="&quo ...

  5. C# Selenium 破解腾讯滑动验证

    什么是Selenium? WebDriver是主流Web应用自动化测试框架,具有清晰面向对象 API,能以最佳的方式与浏览器进行交互. 支持的浏览器: Mozilla Firefox Google C ...

  6. NetSec2019 20165327 exp1+ 逆向进阶

    NetSec2019 20165327 exp1+ 一.实验内容介绍 第一个实践是在非常简单的一个预设条件下完成的: (1)关闭堆栈保护 (2)关闭堆栈执行保护 (3)关闭地址随机化 (4)在x32环 ...

  7. git checkout branch

    git fetch origin feature/banch1:feature/banch1 git checkout feature/banch1 git branch -u origin/feat ...

  8. vue2 作用域插槽slot-scope详解

    插槽分为单个插槽,具名插槽,还有作用域插槽,前两种比较简单这里就不赘述了,今天的重点是讨论作用域插槽. 简单来说,前两种插槽的内容和样式皆由父组件决定,也就是说显示什么内容和怎样显示都由父组件决定: ...

  9. 摩羯座Capricornus

    Capricornus  摩羯座的人通常会如何拒绝别人. 摩羯座的人做事脚踏实地,比较固执,忍耐力也是出奇的强大,同时也非常勤奋.他们心中总是背负着很多的责任感,但往往又很没有安全感,不会完全地相信别 ...

  10. js 获取二级域名

    js获取页面完整地址: window.location.href; var s =" https://ejym.baidu.com";            var h = s.s ...