6.170 / Spring 2001 / Lab 7: Design Patterns

Handout B7

Contents:


Purpose

This lab introduces two common design patterns (Interpreter and Visitor) and their implementations in Java.

Interpreter Pattern

The intent of the Interpreter pattern is, given a language, to define a representation for its grammar. Every language is defined by a set of grammar rules. For example, some simple grammar rules that describe the English language are that sentences have to have a subject phrase and a verb phrase. A subject phrase is made up of either a noun, or a pronoun, etc... The interpreter pattern seeks to take a set of grammar rules that define a language and use it to define a class hierarchy to represent statements in that language. The pattern uses a class to represent each grammar rule in the language. For example, consider the following simple grammar describing simple arithmetic operations defined in BNF (Backus Naur Form):
expression       ::= literal | binary_operation | unary_operation
binary_operation ::= {expression '+' expression} | 
                     {expression '-' expression} | 
                     {expression '*' expression} | 
                     {expression '/' expression}
unary_operation  ::= '-' expression
literal          ::= {0|1|2|3|4|5|6|7|8|9}+ {'.'{0|1|2|3|4|5|6|7|8|9}+}?

This description basically says that the grammar is composed of expressions. Each expression is either a literal, a binary_operation, or a unary_operation. A binary_operation is then defined as an expression followed by either a '+', '-', '*' or '/'. A unary_expression is a '-' followed by an expression

The Interpreter pattern would use a class to represent each of the grammar rules defined above. Symbols on the right-hand side of the rule are instance variables of the classes that represent the left-hand side of each rule. So this grammar would be represented by 4 classes: (abstract) Expression, BinaryOperation, UnaryOperation, and Literal.

abstract class Expression {
  ...
}

class BinaryOperation extends Expression {
  ...
}

class UnaryOperation extends Expression {
  ...
}

class Literal extends Expression {
  ...
}

Often, the Interpreter pattern is used when there is a language to interpret, and you can represent statements in the language as abstract syntax trees. The Interpreter pattern works best when:

It is common for implementations of the Interpreter pattern to define an interpret() operation in each of the syntax tree classes. In that manner, interpret() operations at each node can use the context to store and access the state of the interpreter as it traverses the syntax tree. However, you don't have to define the interpret() operation in the expression classes. If it's common to create a new interpreter, then it is better to use the Visitor pattern to move the operation into a separate Visitor object.

The advantage of the Interpreter pattern is that it makes it easy to change and extend the grammar. Because the pattern uses clauses to represent grammar rules, you can use inheritance to change or extend the grammar. Existing expressions can be modified incrementally, and new expressions can be defined as variations on old ones. The disadvantage is that it is difficult to add new operations to the expression tree as it requires adding it to each expression class.

Visitor Pattern

The intent of the visitor pattern is to represent an operation to be performed on the elements of a class hierarchy. Visitor lets you define a new operation without changing the classes of the elements on which it operates.

Consider a class hierarchy of Nodes (an AbstractNode, and several subclasses of AbstractNode). Every AbstractNode can contain zero or more sub-AbstractNodes as children. Let's say that every AbstractNode defines several operations that can be performed on it, and consequently its children. (For example, a printYourself() method that prints each node in the hierarchy tree.) The problem here is that distributing all these operations across the various node classes leads to a system that is hard to understand, maintain, and change. It will be confusing to have code for multiple Node operations mixed together especially because they are all unrelated one another. Moreover, adding a new operation will require touching and recompiling all of these classes. It would be better if each new operation could be added separately, and the node classes were independent of the operations that apply to them.

We can have both by packaging related operations from each class in a separate object, called a visitor, and passing it to elements of the Node hierarchy as it is traversed. When an element "accepts" the visitor, it sends a request to the visitor that communicates its runtime type. This is often called a double-dispatch mechanism.

As a consequence of separating the operation logic from the class hierarchy on which the visitor operates, it makes it relatively easy to define new operations on the class hierarchy. It is also easy to then classify and gather related operations together and separate unrelated operations through a separate Visitor class hierarchy. However, the Visitor pattern makes adding classes to the Node hierarchy difficult, as each new Node class gives rise to a new abstract operation on Visitor and a corresponding implementation in each of its subclasses.

Descriptions of the design patterns were largely borrowed from Design Patterns: Elements of Reusable Object-Oriented Software by Gamma, Helm, Johnson, and Vlissides (more commonly referred to as the "Gang of Four" book), which contains more detailed information about these and other design patterns.


An Example: Calculator

In this set of exercises, you will examine a fairly complex "MATLAB"-like calculator system that makes use of the Interpreter and Visitor patterns discussed above and add some features to the system making use of the design patterns.

Setup

First, copy the files from /mit/6.170/lab7/ to your ~/6.170/lab7 directory and compile them.
mkdir ~/6.170/lab7 
cd ~/6.170/lab7
cp /mit/6.170/www/labs/lab7/*.java .
javac *.java

Running the Code

Examine the specifications for the calculator system. The calculator excepts arithmetic expressions such as the ones specified above, and also the following commands:

You can start the calculator by invoking:

java lab7.Driver

Now take the calculator for a spin. Enter some commands like:

COMMAND =>  3-4
COMMAND =>  LET x = 25
COMMAND =>  x + 25
COMMAND =>  x - 3
COMMAND =>  LET y = 3*x
COMMAND =>  y
COMMAND =>  LET x = 4
COMMAND =>  y
COMMAND =>  WHO
COMMAND =>  QUIT

Exercise 1: Object models

command          ::= clear | let | 'WHO' | expression
let              ::= 'LET' var '=' expression
clear            ::= 'CLEAR' {var}?
expression       ::= literal | binary_operation | unary_operation | var
var              ::= {character}({character}|{digit})*
binary_operation ::= {expression '+' expression} | 
                     {expression '-' expression} | 
                     {expression '*' expression} | 
                     {expression '/' expression}
unary_operation  ::= '-' expression
literal          ::= {digit}+ {'.'{digit}+}?
'QUIT' is also a valid command, and causes the Driver to exit immediately

Draw a Problem Object Model for the calculator grammar that is described above.

The Interpreter pattern makes the class hierarchy parallel the grammar definition. This makes it easier to define operations that rely on the rules of the grammar. To see this point, suppose that the implementation of the grammar looked like this:

How would that affect the implementation of the EvalVisitor?

Hint: think about how each hierarchy would evaluate the following expressions trees (assuming that the parser allowed them as input):

COMMAND =>  LET x = LET x = 3

COMMAND =>  3 + CLEAR

COMMAND =>  LET x = 3 + WHO

Exercise 2: Outputting expressions in prefix form

In this exercise, you will write PrefixPrintVisitor, which prints out expressions in prefix notation, also sometimes known as "Polish notation".

Examples of prefix notation are:

<+ 2 4>             // corresponds to "2 + 4" infix
<+ x <* 4 5>>       // corresponds to "x + 4 * 5" infix

Modify the method getPrintVisitor() in Driver.java to return your PrefixPrintVisitor instead.

Exercise 3: Sequencing definitions

In this exercise, you will correct a deficiency of the current system. Start by executing the following commands:
COMMAND =>  CLEAR
COMMAND =>  LET x = 5
COMMAND =>  LET y = x + 2
COMMAND =>  y
COMMAND =>  LET x = 10
COMMAND =>  y
What output do you get? Why? Is the output surprising?

Now try this set of commands:

COMMAND =>  CLEAR
COMMAND =>  LET x = y + 2
COMMAND =>  LET y = 3 * x
COMMAND =>  y
What happened and why? Perhaps an examination of EvalVisitor will shed some light.

Modify EvalVisitor, or create another Visitor class to curtail this aberrant behavior by throwing an EvalVisitor.RecursiveDefinitionException. This is a little tricky; think about how the EvalVisitor traverses the expression tree and how it might go about dealing with this issue.

If you get stuck on this exercise, you can copy the staff implementation by doing:

cp /mit/6.170/www/labs/lab7/.EvalVisitor.class ~/6.170/lab7/EvalVisitor.class

Exercise 4: Outputting expressions as textual trees

Write an AsciiArtPrintVisitor that produces a visually pleasing representation of the expression. For example, you can try to represent an expression like: 3 + 4 * (x + 5) as:
     +
    / \
   3   *
      / \
     4  ( )
         |
         +
        / \
       x   5

There are many other more creative representations, the key is for you to become more familiar with the ease of defining new operations on the syntax tree.


Back to the 6.170 labs index.
Back to the 6.170 home page.
For problems or questions regarding this page, contact: 6.170-webmaster@mit.edu.