Tuesday, May 15, 2007

Compositional Visitor

Overview:The Compositional Visitor replaces the Visitor class hierarchy with a compositional pattern where each Visitable object has its own (very simple) Visitor interface - and each Visitor picks and chooses which Visitor interfaces it implements and hence which elements it operates on. If a Visitor does not need to process a particular element then it does not implement its associated Visitor interface. Knowledge of the standard Visitor Pattern is assumed.Background: The traditional Visitor pattern is flawed for many real world applications as it scales badly. As it says in Design Patterns:Consequences (of the Visitor Pattern):3. Adding new ConcreteElement classes is hard.... each new ConcreteElement gives rise to a new abstract operation on Visitor and a corresponding implementation in each ConcreteVisitor class.You are left with two choices either:a) You have a separate Visitor interfaces (or class hierarchies) for each 'type' of Visitor. (Visitors of the same 'type' being Visitors that process the same elements) - and a corresponding Visitable interface for each 'type', orb) (The orthodox approach) Implement visit(element) for every possible (Visitable) element in every Visitor.Neither way is elegant. It is possible to have a class hierarchy of Visitors with a superclass that implements default implementations of visit for every Visitable element; but this is far from ideal as that superclass still needs to be maintained and the Visitors are aligned in a hierarchy based on the fact that they are Visitors not based on their role or requirements within the application.In essence the Visitors and Visitable objects should be INDEPENDENT - so the Visitable objects should not care if a new Visitor is added - and a Visitor should not care if a new Visitable object is added. Furthermore the Visitors themselves should be independent - unless they share common processing requirements.Compositional Visitor Pattern:The secret to making the Visitors and Visitable objects independent is to use composition. There is only one visitable interface:public interface Visitable {public Object accept(Object visitor) throws Exception;}Each visitable element has its own Visitor interface (which could potentially be created using code generation as they are very simple) e.g.public interface Element1Visitor {public Object visit(Element1 visitable) throws Exception;}public interface Element2Visitor {public Object visit(Element2 visitable) throws Exception;}Each Visitable element implements Visitable. The accept method checks to see if the visitor implements its Visitor - if so then is passes itself to the Visitor for processing - if not then it either passes the request up the class hierarchy (if the superclass implements Visitable) or returns null (or conceivably throws a checked NotProcessableByVisitorException). public class Element1 implements Visitable {//Other properties and methodspublic Object accept(Object visitor) throws Exception {if (visitor instanceof Element1Visitor) {return ((Element1Visitor)visitor).visit(this);}else {return null;//For subclasses of Visitable classes// return super.accept(visitor)}}}public class Element2 implements Visitable{//Other properties and methodspublic Object accept(Object visitor) throws Exception {if (visitor instanceof Element2Visitor) {return ((Element2Visitor)visitor).visit(this);}else {return null;//For subclasses of Visitable classes// return super.accept(visitor)}}}The Visitor itself simply implements the Visitor interfaces for the Visitable Elements it can process:public class SampleVisitor implements Element1Visitor, Element2Visitor {public Object visit(Element1 visitable) throws Exception {Object result = null;//Do work for element1return result;}public Object visit(Element2 visitable) throws Exception {Object result = null;//Do work for element2return result;}}The Visitor can then be used as normal:SampleVisitor sampleVisitor = new SampleVisitor();for (Visitable visitable : visitableObjects){visitable.accept(sampleVisitor);}ConclusionWhilst this pattern does create some additional classes (the extra Visitor interfaces) - they are simple and could be automatically generated. It also has a more complex accept method than just a straight callback to the Visitor; but it is far from complicated and only needs to be written once to handle all possible Visitors. The big benefit, on the other hand, is that you can use the Visitor pattern without worrying about adding new Visitable elements or Visitors. In fact ANY class can become a visitor just by implementing the appropriate interfaces for whatever elements you want to process. Visitors become cheap and easy.But why use the Visitor pattern anyway? Because Visitors are a great way to centralise specific processing and remove logic (especially business logic) from model objects. This is a boon for Test Driven Development becausea) It assists with keeping certain classes (e.g. model objects) clean and free from processing logic so they can then be used freely throughout test code without worrying about mocking them or about their behaviour changing. b) The processing logic is all in one spot (the Visitor) and hence can be more easily, and neatly, tested than if it were scattered throughout the Visitable entities.I believe that in general putting logic within model classes is bad idea - and as soon as other system resources are required (e.g. a Rules Engine) you cannot put the logic there. Visitors offer - to my mind - a much more elegant solution.

No comments: