Visitor Design Pattern

0

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.

 

http://hatha-yoga-poses.blogspot.in/

 

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.

http://hatha-yoga-poses.blogspot.in/

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.

http://hatha-yoga-poses.blogspot.in/

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);
    }
}

About Author

Ram’s expertise lies in test driven development and re-factoring. He is passionate about open source technologies and loves blogging on various java and open-source technologies like spring.
You can reach him at [email protected]

Leave A Reply