Visitor Pattern allows adding new methods to existing hierarchies without modifying the structure. One can dynamically add new behavior to the class. Decorator Pattern is another pattern in this family.
Visitor Pattern Example
Our example deals with a student enrolling to a painting course. There are number of courses available, like Pastel Course, Oil painting course and water color course. Enrollment Manager enrolls a student to a painting course. There are now few rules which governs the limit and cost of the course.
Rues are:
- If it is pastel course, for the first 10 students, there is a discount.
- In case of watercolor course, student is allowed to bring their own material. In which case, there will be a cost reduction of 200. In all other courses, management will supply the material.
- Management has observed that they can afford more people in pastel courses as the space requirement is not so much.
Here is my Course interface:
Course
package patterns.visitor; public interface Course { void join(Student s); Student[] getStudents(); int getLimit(); int getCost(); void incrementLimit(int additional); }
Here is the code for OilCourse. Note other courses will be in similar lines so I am not pasting the code here.
OilCourse
package patterns.visitor; import java.util.HashSet; import java.util.Set; public class OilCourse implements Course { private Set students = new HashSet(); private int limit; public OilCourse() { limit = 15; } @Override public void join(Student s) { students.add(s); } @Override public Student[] getStudents() { return students.toArray(new Student[students.size()]); } @Override public int getLimit() { return limit; } @Override public int getCost() { return 2000; } @Override public void incrementLimit(int i) { limit += i; } }
CourseCostAduster is the class which adjusts the cost per course based on the various rules.
package patterns.visitor; public class CourseCostAdjuster { void adjustCost(Course inCourse, Student inStudent) { if (inCourse instanceof OilCourse) { if (inCourse.getLimit() < 10) { inStudent.setCost(inCourse.getCost() - inCourse.getCost()*10/100); } } if (inCourse instanceof WatercolorCourse) { if (inStudent.ownMaterial()) { inStudent.setCost(inCourse.getCost() - 200); } } } }
LimitAdjuster adjusts the upper cap of the class.
package patterns.visitor; public class LimitAdjuster { void adjustLimit(Course inCourse) { if (inCourse instanceof PastelCourse) { if (inCourse.getStudents() != null && inCourse.getStudents().length == inCourse.getLimit()) { inCourse.incrementLimit(inCourse.getLimit() + 5); } } } }
LimitValidator validates whether the limit is violated.
package patterns.visitor; public class LimitValidator { void validateLimit(Course inCourse) { if (inCourse.getStudents() != null && inCourse.getStudents().length == inCourse.getLimit()) { throw new RuntimeException("Limit reached, can't enrol anymore"); } if (inCourse instanceof PastelCourse) { if (inCourse.getStudents() != null && inCourse.getStudents().length == 40) { throw new RuntimeException("Limit reached, can't enrol anymore"); } } } }
EnrolmentManager does the job enrolling a student. It adjusts the cost and adjusts the upper limit if needed.
package patterns.visitor; public class EnrolmentManager { public void enrol(Student inStudent, Course inCourse) { new CourseCostAdjuster().adjustCost(inCourse, inStudent); new LimitAdjuster().adjustLimit(inCourse); inCourse.join(inStudent); } }
The main problem with the above design is we have rules which keeps coming and for every rule we need to do the type check on course to do the course specific logic. There is a dispatching involved which we are managing ourselves and not using the power of polymorphic.
We will now use Visitor pattern to dynamically dispatch the logic to the right rule validating classes.
All the rules have one pattern common in them and that is, the dispatching logic (if pastel then…). Based on this pattern, we come up with the below CourseVisitor interface
Mbr/>
package patterns.visitor; public interface CourseVisitor { void visit(OilCourse oilCourse, Student student); void visit(PastelCourse pastelCourse, Student student); void visit(WatercolorCourse watercolorCourse, Student student); }
Each rule will now implement the above visitor interface.
CostAdjuster
package patterns.visitor; public class CostAdjusterVisitor implements CourseVisitor { @Override public void visit(OilCourse oilCourse, Student student) { if (oilCourse.getLimit() < 10) { student.setCost(oilCourse.getCost() - oilCourse.getCost()*10/100); } } @Override public void visit(PastelCourse pastelCourse, Student student) { } @Override public void visit(WatercolorCourse watercolorCourse, Student student) { if (student.ownMaterial()) { student.setCost(watercolorCourse.getCost() - 200); } } }
LimitAdjusterVisitor
package patterns.visitor; public class LimitAdjusterVisitor implements CourseVisitor { @Override public void visit(OilCourse oilCourse, Student student) { } @Override public void visit(PastelCourse pastelCourse, Student student) { if (pastelCourse.getStudents() != null && pastelCourse.getStudents().length == pastelCourse.getLimit()) { pastelCourse.incrementLimit(pastelCourse.getLimit() + 5); } } @Override public void visit(WatercolorCourse watercolorCourse, Student student) { } }
LimitValidatorVisitor
package patterns.visitor; public class LimitValidatorVisitor implements CourseVisitor { @Override public void visit(OilCourse oilCourse, Student student) { validateLimit(oilCourse); } @Override public void visit(PastelCourse pastelCourse, Student student) { if (pastelCourse.getStudents() != null && pastelCourse.getStudents().length == 40) { throw new RuntimeException("Limit reached, can't enrol anymore"); } } @Override public void visit(WatercolorCourse watercolorCourse, Student student) { validateLimit(watercolorCourse); } private void validateLimit(Course course) { if (course.getStudents() != null && course.getStudents().length == course.getLimit()) { course.incrementLimit(course.getLimit() + 5); } } }
With this change we now have all rules in proper methods. In a way, we are partially successful in dissolving the ‘if instanceof PastelCourse’ and else dispatching pattern. Now our next task is to apply the rule to the course.
We will now introduce a new method to Course interface to accept the rule.
package patterns.visitor; public interface Course { ... void accept(CourseVisitor visitor, Student student); }
The method accept(visitor)
will simply pass itself to the visitor object. Due to polymorphism, the control will automatically get dispatched to the right visitor class.
package patterns.visitor; import java.util.HashSet; import java.util.Set; public class OilCourse implements Course { ... @Override public void accept(CourseVisitor visitor, Student student) { visitor.visit(this, student); } }
The visit method as you know is overloaded and there are multiple versions available in Course interface. Again due to polymorphism, the control will get dispatched to the right visit method. Thus in visitor pattern, there is dual dispatch, first the visitor and then the visited.
Finally, the EnrolmentManager will look like below:
package patterns.visitor; public class EnrolmentManager { public void enrol(Student inStudent, Course inCourse) { inCourse.accept(new CostAdjusterVisitor(), inStudent); inCourse.accept(new LimitAdjusterVisitor(), inStudent); inCourse.accept(new LimitValidatorVisitor(), inStudent); inCourse.join(inStudent); } }