OOP Class Notes for 10/17/13
The Expression Problem
- We want to represent a graph-like structure (e.g., the
syntax tree of expressions in our calculator).
- Each node has a type (number, addition, etc.) and we have
operations on the graph that depend on these types (e.g.,
expression evaluation and pretty printing).
- How do you implement the operations? How do you implement the types?
- You can only really have easy development of one (types
or operations). This is called the Expression
Problem.
Extensible Types
- If you want extensibility of types, use the classic OOP implementation:
- You have an abstract super class for
nodes with abstract methods for every operation:
abstract public class Expression {
abstract public int eval();
abstract public void print();
}
- To add a new type of node, make a new class that extends the abstract super class.
- Subsequently, this class has a method for every operation you can do on that node.
public class Number extends Expression {
final int value;
public Number(int value) { this.value = value; }
public int value() { return value; }
public int eval() { return value(); }
public void print() { System.out.print(value()); }
}
abstract public class BinaryExpression extends Expression { ... }
public class Addition extends BinaryExpression {
public int eval() { return left().eval() + right().eval(); }
public print() {
System.out.print("(");
left().print();
System.out.print(" + ");
right().print();
System.out.print(")");
}
}
...
- This is bad for maintainability of operations,
since the implementation of a single operation
is scattered over many node type classes.
- To add a new operation on nodes, you
have to edit every node class and add
the new method.
- Ideally, we also want extensibility
of operations, i.e., the
implementation of each operation
should be encapsulated
in its own class.
- Extensibility of operations is often
more important than extensibility of
types.
Extensible Operations
- A first idea to implement extensible operations would
be to use method overloading:
public static class Evaluator {
public int eval(Number num) {
return num.value();
}
public int eval(Addition add) {
return eval(add.left()) + eval(add.right());
}
...
}
Unfortunately, this code does not compile.
The Java compiler resolves calls to overloaded methods
such as eval
statically based on the static
types of the arguments and result of the call:
- The static type of
add.left()
passed to the
first call to eval
in eval(Addition
add)
is Expression
.
- However, there is no method with the
signature
public int eval(Expression)
.
To implement extensible operations on
nodes, we need to be able to dispatch calls to
methods based on the dynamic types of
the node objects that are passed as arguments to
the methods.
The Visitor Design Pattern
- If you want extensibility of
operations, use the Visitor Design
Pattern
- We have visitors and node classes.
How does the Visitor Design Pattern work?
- To implement extensible operations, we
need double dispatching: we want to dispatch calls to the
visit
method based on
- the dynamic type of
the
Visitor
object; and
- the dynamic type of the node object that is
passed to the
visit
method.
- Double dispatching is realized in the Visitor
Pattern by
introducing an additional level of indirection,
namely, by introducing the additional call to the
method
accept
. The call
to accept
first dispatches based on the
dynamic type of the node, and then it calls
back visit
to dispatch based on the
dynamic type of the visitor.
- Let's look at how this works, step by step:
- Suppose the
visit
method of
a Visitor
calls
the accept
method of some node
object, say of dynamic
type Addition
.
- The call to
accept
is
dynamically dispatched to the specific
implementation of the accept
method
in the specific node class; here the class Addition
.
-
The dynamic
type of the node object on
which
accept
is called is also the
static type of this
inside of the
implementation of the accept
method to which the call is dispatched; here in
class Addition
.
-
Thus, when the
accept
method of
class Addition
calls back to
the visit
method
using visitor.visit(this)
, the
compiler uses the static type
of this
, to determine statically the right
variant of the overloaded visit
method in the Visitor
interface to
which the call should go,
namely visit(Addition)
.
- Now, to add a new operation, we only need to
implement a new visitor class.
- That is, you can now add a new operation without
changing the node classes on which the
operations will work on.
- Drawback: to add a new type of nodes, you now
have to edit every
Visitor
class to add
a new visit(Type)
method.
Though, this is often the lesser of two evils.