Visitor 模式心得
最近讀到Visitor模式,還是一知半解的。偶然翻到Uncle Bob對(duì)該模式的推導(dǎo)過(guò)程,有所心得,和大家分享一下。 Uncle Bob 的鏈接是: http://butunclebob.com/ArticleS.UncleBob.VisitorVersusInstanceOf。個(gè)人覺(jué)得該模式用來(lái)操作復(fù)雜對(duì)象集合,特別適用于報(bào)表生成。因?yàn)閳?bào)表的來(lái)源相對(duì)穩(wěn)定(復(fù)雜數(shù)據(jù)集合),但是表現(xiàn)形式卻是千變?nèi)f化。言歸正傳,我將該博客的內(nèi)容按照自己的理解分享出來(lái),如果有什么不對(duì)的地方,請(qǐng)指正。
首先有一個(gè)如下簡(jiǎn)單的場(chǎng)景,Bob先生的公司提供計(jì)算機(jī)方面的培訓(xùn)服務(wù),對(duì)社會(huì)人士開(kāi)設(shè)二門(mén)課程,一門(mén)是OOD, 一門(mén)是Java,java課程需要上機(jī)(結(jié)對(duì)編程,所以二個(gè)人用一臺(tái)機(jī)器),所以需要根據(jù)報(bào)名的時(shí)間計(jì)算具體的機(jī)器數(shù)目:

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; } }
代碼實(shí)現(xiàn)了,B先生很happy。不久客戶(hù)說(shuō)需要生一份報(bào)表,B先生想了5分鐘,洋洋灑灑寫(xiě)下如下代碼:

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()); } }
報(bào)表很簡(jiǎn)單, 打印結(jié)果如下, 格式為: 時(shí)間 + 名稱(chēng) + 數(shù)量
Report is : 04/20/2015 AOOD 0 Computers
04/20/2015 Java 5 Computers
B洋洋得意,客戶(hù)又來(lái)了新的需求,前面的報(bào)表太簡(jiǎn)單了,需要針對(duì)不同的課程打印不同的內(nèi)容:

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 類(lèi)圖可以看出,CourseComputerReport 因?yàn)閷?duì)每門(mén)課程的format不一致,需要遍歷的時(shí)候針對(duì)不同的課程設(shè)置不同的格式從而導(dǎo)致CourseReport和Course之前出現(xiàn)強(qiáng)耦合, 可以通過(guò)重構(gòu)方法 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)); } }
老鳥(niǎo)L看了B的實(shí)現(xiàn)后,提出了自己的看法,為什么 CourseReport 需要依賴(lài)具體的Course, 能否將Course告訴reporter 而不是Reporter來(lái)判斷具體的Course。 并將Visitor的經(jīng)典類(lèi)圖隨手拋給小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); } }
老鳥(niǎo)L看了以后表示滿意,同時(shí)指出如果添加新的Course,由于所有的course都依賴(lài)CourseVisitor,而courseVisitor需要添加新的接口,所有的course需要重新打包和編譯。是否可以新建一個(gè)空接口CourseVisitor 使Course依賴(lài)這個(gè)接口從而達(dá)到隔離變化的目的。需要修改的是需要將courseVisitor Cast到具體的實(shí)現(xiàn)類(lèi),主要的代碼實(shí)現(xiàn)是:
@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); } }
順便說(shuō)一下,上面使用的模式名稱(chēng)是Acyclic visitor, 是為了解決添加新的item 導(dǎo)致所有的item需要重新編譯和打包的問(wèn)題。
posted on 2017-05-31 13:11 布兜兜 閱讀(489) 評(píng)論(0) 收藏 舉報(bào)
浙公網(wǎng)安備 33010602011771號(hào)