The third lecture on recursive data types. This lecture answers the question posed at the end of the last lecture, creating a more flexible form of expression tree.
If the class hierarchy representing the recursive data type isn't going to change much but we might want to add more and more operations, it may make sense to define a corresponding Visitor class hierarchy and add appropriate accept() methods to the classes of the original hierarchy. The main idea of the Visitor design pattern is to "rotate" by 90° from the derivatives (subclasses in the original class hierarchy, here it's Expression) to the methods in the parallel Visitor hierarchy. The new functionality can be added by creating a new subclass in the Visitor hierarchy.
Here is the abstract class (or, the interface) that we would need for Expressions. Notice that we need a routine for each of the classes in the original hierarchy, and each method takes one parameter which is an instance of that class.
public interface ExpressionVisitor {
public void visitConstant(Constant4 c);
public void visitAddition(Addition4 a);
public void visitMultiplication(Multiplication4 m);
public void visitNegation(Negation4 n);
}
Class Expression4 is now pretty simple. It too can be declared as an interface if we have nothing to put in there except for method declarations and final (constant) fields (in this case, the child classes will implement this interface; or it can be declared as abstract, in which case he child classes extend this parent class.
public abstract class Expression4 {
public abstract void accept(ExpressionVisitor visitor);
}
The different subclasses implement accept(), which tells expression visitors what type they are and thus which of their routines to run on them.
public class Constant4 extends Expression4 {
int value;
public Constant4(int v) {
value = v;
}
public void accept(ExpressionVisitor visitor) {
visitor.visitConstant(this);
}
}
public class Addition4 extends Expression4 {
Expression4 left, right;
public Addition4(Expression4 l, Expression4 r) {
left = l;
right = r;
}
public void accept(ExpressionVisitor visitor) {
visitor.visitAddition(this);
}
}
Classes for Multiplication and Negation are similar.
Note that the fields in these classes (value in Constant and left and right in Addition) have no visibility modifier (public, protected or private). This means that these fields have package visibility. Only instances of classes in the same package can access this field. If we organise our program so that Expression and its subclasses, and ExpressionVisitor and its subclasses are all in the same package (and nothing else is), then this will work correctly. Those fields will be accessible to the classes that need access, but hidden from everything else. Alternatively we would have to do the usual nonsense with making the fields private and having public accessor methods.
Now we can define visitors to evaluate and format Expressions.
public class ExpressionEvaluator implements ExpressionVisitor {
/** The calculated value */
private int value;
/** public view of the value */
public int getValue() { return value; }
/** Get the value of a constant expression */
public void visitConstant(Constant4 c) {
value = c.value;
}
/** Get the value of a sum */
public void visitAddition(Addition4 a) {
ExpressionEvaluator leftEvaluator = new ExpressionEvaluator();
ExpressionEvaluator rightEvaluator = new ExpressionEvaluator();
a.left.accept(leftEvaluator);
a.right.accept(rightEvaluator);
value = leftEvaluator.value + rightEvaluator.value;
}
/** Get the value of a product */
public void visitMultiplication(Multiplication4 a) {
ExpressionEvaluator leftEvaluator = new ExpressionEvaluator();
ExpressionEvaluator rightEvaluator = new ExpressionEvaluator();
a.left.accept(leftEvaluator);
a.right.accept(rightEvaluator);
value = leftEvaluator.value * rightEvaluator.value;
}
/** Get the value of a negation */
public void visitNegation(Negation4 n) {
n.expression.accept(this);
value = -value;
}
}
public class ExpressionFormatter implements ExpressionVisitor {
/** The formatted string being built */
private String string;
/** Public access to the string */
public String getString() { return string; }
/** Initialise with an empty string */
public ExpressionFormatter() {
string = "";
}
/** Get the string for a constant */
public void visitConstant(Constant4 c) {
string += c.value;
}
/** Build the string for a sum */
public void visitAddition(Addition4 a) {
string += "(";
a.left.accept(this);
string += " + ";
a.right.accept(this);
string += ")";
}
/** Build the string for a product */
public void visitMultiplication(Multiplication4 a) {
string += "(";
a.left.accept(this);
string += " * ";
a.right.accept(this);
string += ")";
}
/** Build the string for a negation */
public void visitNegation(Negation4 n) {
string += "-(";
n.expression.accept(this);
string += ")";
}
}
This design leads to a delicate (confusing?) exchange of messages between the objects in an expression tree and the objects performing operations on them.
The reason for this apparent complexity is that this is an example of double (dual) dispatch (called so because it involves two polymorphic dispatches). If I make the routine call ‘expression.accept(visitor)’, the actual operation that is carried out depends on two runtime types: the actual type of ‘expression’ and that of ‘visitor’.
(Usually only the type of the target matters. That is, in a method call a.b(), the version of b() that actually executes is determined at runtime based on the actual type of a. This is called dynamic dispatch.)
Expression4 a, b, c, d, e; ExpressionFormatter f = new ExpressionFormatter(); ExpressionEvaluator v = new ExpressionEvaluator(); a = new Constant4(1); b = new Constant4(2); c = new Addition4(a, b); a = new Constant4(3); b = new Constant4(4); d = new Addition4(a, b); e = new Multiplication4(c, d); e.accept(f); e.accept(v); System.out.println(f.getString() + " = " + v.getValue());
+ Class Expression is now very simple.
+ Easy to create new visitors for Expression without modifying any existing classes.
+ No subtle manipulation of object references.
+ Each expression type has only relevant attributes.
+ All the code for a particular operation is in the one concrete subclass of ExpressionVisitor, rather than spread across all the different subclasses of Expression.
- Interaction between Expression and ExpressionVisitor is subtle.
- Difficult to add new kinds of expression, e.g., division.
ExpressionVisitor must be modified to add a new routine for division.
All subclasses (here ExpressionEvaluator and ExpressionFormatter) must now implement the new routine.
This style of interaction is called the Visitor pattern.
The Visitor DP (Design Pattern) works well if the visited hierarchy (Expression) is stable, ie, does not undergo modification (at least, not too often). The reason is that the described above Visitor design pattern introduces a cyclic dependency which ties together all the visited derivatives (subclasses of Expression). Namely, the Expression interface, and therefore, all the subclasses in the visited hierarchy, depends on the Visitor interface (all the visit methods). If we add one subclass to the visited hierarchy, the Visitor interface must change to include the visit method for the new subclass. Since the Expression depends on the Visitor, every subclass of Expression depends on Visitor, and thus if we add even one small class to Expression hierarchy, then the whole big hierarchy must be recompiled. The latter operation may be costly, and can sometimes be impossible if we do not have access to the original source code.
There are ways to address this problem. One modification of the Visitor DP (known as Acyclic Visitor) involves multiple inheritance and runtime type identification.
This example leads us to some more general discussion about what qualities we look for in the different design choices for data structures.
When evaluating the design of any package of software, we look for some additional qualities beyond the usual ones of any software (correctness, ease of maintenance, modularity, readability (and more)).
Two important properties that apply to our solutions for representing simple expressions are that the set of class definitions we use should be:
Encapsulation is improved by keeping our code in well-organised, coherent classes and methods. This means that each class (and each method) should express one set of strongly related ideas, and other ideas should be in other classes (or methods). Information hiding means that the class or method should reveal (make public) as little as possible: the minimum interface for users of the class or method. As much as possible should be hidden away inside the class or method by keeping it inside the method or being in private methods or inner, private classes.
Encapsulation is a property of our model of the outside world (that is, how we design the abstract idea of an Expression) but is also a strong property of how we capture that model in classes and methods. In this case our abstraction of an expression is a tree of operators and constants or operands. You have seen in these examples that the same abstraction can be represented in several different ways, with different combinations and relationships of classes and methods. The different versions of Expression have different ways of encapsulating the idea of "evaluate" and "create" in methods, and different ways of encapsulating the information that is associated with the different cases: constants, additions, multiplications, negations.
Extendability can be measured against the wider world of the problem space. What do we mean by extending the world of Expressions? There are two directions: one is to add more kinds of expressions; the other is to add more functionality beyond the first examples we have seen, pretty printing and evaluating.
More kinds of expressions can be added by adding operators. I mentioned the example of exponentiation, which is another operator that has two operands: the expression "5 to the power 6" is written in various notations as 5^6 or 5**6 or 56, and in our models it can be represented as another operation just like ADDITION. The difference is in pretty-printing, but only because rendering a superscript is difficult in the methods of constructing a text string that we have used so far).
An example with a different structure is the conditional expressions like
those in the C language, which have 3 operands, as in the expression
( (a < b) ? 42 :5+7)
which chooses the 2nd or 3rd operand depending on whether the first
operand evaluates as true or false. In this example the value of this
expression is: if the value of a is less than the value of b then the result is 42,
otherwise it is the value of (5+7).
This kind of expression can be represented as an Expression
which has an operator CONDITIONAL with three subexpressions.
This might be represented in Java as fields
Expression condition,
truevalue, othervalue;
The other kind of extension is in the functions that we implement on the Expressions. For example, a more powerful way to print an expression would be to convert it into HTML or another text markup notation and view the result through a browser: in this way we could produce a rendering of italics for variable names, superscripts for exponentiation, and so on. Our basic pretty print might deliver the string
(a + (b^2) x 3)
but the HTML version could produce
<span style="font-family:courier">( <em>a</em> + ( <em>b</em> <sup>2</sup> ) <span style="font-family:sans">x</span> 3 )</span>
which a browser will render as
( a + ( b 2 ) x 3 )
Other translations of expressions might include: produce instructions to evaluate the expression on a computer (i.e. compile the expression, rather than interpreting it); or automatically display the tree structure in graphic format, like the examples I had to draw by hand for lectures.
Either kind of extension of Expressions means revising and writing program code. How much code needs to be changed, how many classes or methods need to be changed, or simply new ones added, make a big difference in how quickly the change can be made, and how safely, with less risk of introducing new bugs by changing existing code in the wrong way. This is an important property of the Visitor method: it changes the numebr of classes and methods we would otherwise have to add when we add functionality to our representation of Expressions.
Because the methods in the Visitor interface all have different signatures, we could overload the definitions and not invent new names for each one:
public interface ExpressionVisitor {
public void visit(Constant4 c);
public void visit(Addition4 a);
public void visit(Multiplication4 m);
public void visit(Negation4 n);
}
You can decide for yourself whether this is easier to create, and easier to use (is it any more prone to programming errors?)
The topic of Design Patterns in general is one of the subjects discussed in the Comp2110/2510/6444 course on Software Design.
Find a book on design patterns and learn about the Visitor pattern.
Look at a functional programming language (e.g. Haskell, Miranda or Lisp) and compare the higher-order functions map and fold with the Visitor pattern. Is the Visitor pattern just a trick for getting around the limitations of languages like Java, Eiffel and C++? or is it something deeper?
Copyright © 2006, 2007 Jim Grundy, Ian Barnes, Richard Walker &
Chris Johnson, The Australian National University
$Revision: 1.4 $ $Date: 2010/02/18 05:30:33 $ $Author: cwj $
Feedback & Queries to
comp2100@cs.anu.edu.au