Visitor 模式心得

最近讀到Visitor模式,仍是隻知其一;不知其二的。偶然翻到Uncle Bob對該模式的推導過程,有所心得,和你們分享一下。 Uncle Bob 的連接是: http://butunclebob.com/ArticleS.UncleBob.VisitorVersusInstanceOf。我的以爲該模式用來操做複雜對象集合,特別適用於報表生成。由於報表的來源相對穩定(複雜數據集合),可是表現形式倒是變幻無窮。言歸正傳,我將該博客的內容按照本身的理解分享出來,若是有什麼不對的地方,請指正。java

首先有一個以下簡單的場景,Bob先生的公司提供計算機方面的培訓服務,對社會人士開設二門課程,一門是OOD, 一門是Java,java課程須要上機(結對編程,因此二我的用一臺機器),因此須要根據報名的時間計算具體的機器數目:android

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\n");
        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

報表很簡單, 打印結果以下, 格式爲: 時間 + 名稱 + 數量app

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

B洋洋得意,客戶又來了新的需求,前面的報表太簡單了,須要針對不一樣的課程打印不一樣的內容:ui

 

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\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));
    }
}
View Code

從上面的UML 類圖能夠看出,CourseComputerReport 由於對每門課程的format不一致,須要遍歷的時候針對不一樣的課程設置不一樣的格式從而致使CourseReport和Course以前出現強耦合, 能夠經過重構方法 generateComputerReport 使之更加合理:this

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\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));
    }
}
View Code

老鳥L看了B的實現後,提出了本身的見解,爲何 CourseReport 須要依賴具體的Course, 可否將Course告訴reporter 而不是Reporter來判斷具體的Course。 並將Visitor的經典類圖隨手拋給小B:spa

小B恍然大悟,修改代碼以下:3d

 

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\n");
    }

    @Override
    public void visit(JavaCourse javaCourse) {
        report.append("Java ");
        report.append(String.valueOf(javaCourse.getTotalComputers())).append(" Computers\n");
    }
}
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到具體的實現類,主要的代碼實現是:code

 @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須要從新編譯和打包的問題。

相關文章
相關標籤/搜索