Last time we took a procedural approach to solve the expression problem.
This time I will demonstrate an object-oriented approach. As a
reminder, we want to model the following forms of expressions,
expr + expr
expr - expr
expr * expr
expr / expr
The most natural thing to do in an object-oriented language is to make each of the above
expression forms its own class. First, I will introduce a base class to provide an expression abstraction.
Now I will now create a sub-class for each expression.
I created a base class for the binary expressions which I will take
advantage of below.
To evaluate the expression I will add an abstract virtual method,
Evaluate(), to Expr.
Then I will override the Evaluate() method for
each of the above classes. Evaluating a literal is simply returning the value of
the literal so I modified the Literal class to,
All binary expressions need to evaluate their left-hand expression and
right-hand expression and then perform their operation on the results. To represent this, I
overrode Evaluate() to evaluate both Left and Right
and then call a newly introduced EvaluateOp()
method. I sealed the Evaluate() method because I want
descendants to override EvaluateOp() not
Now I can implemented the concrete descendents of BinaryExpr.
The advantages an object-oriented approach are,
New data types can be added without affecting any of the other data
Each data type is encapsulated, only the data type needs to have access
the instance variables.
All the operations affecting the instance variables are in one place,
the methods of instance variable's class.
New operations can be added as abstract methods. The compiler will then
generate an error if an operation is not implemented for one of the leaf
Efficient storage is natural. Adding field to one of the expression
types do not affect the others.
To demonstrate how easy it is to add a new data type I will again add support
expr ^ expr
To do this I will add a class to represent the Power expression form.
This looks like,
Note that the Power data type can be added without
modifying the other classes.
The disadvantages to an object-oriented approach are,
It is difficult to add new operations because adding an operation
requires modifying all the classes.
The logic for each operation is spread over multiple classes and often
multiple files. This makes it cumbersome to get a complete picture what the
It is very difficult to dynamically add operations and can't be done
without careful planning.
To demonstrate some of the difficulties of adding a new operation, I will add
the Print operation. First I will modify the base Expr
class to add an abstract Print() method.
Now I will override this method in each class similar to the way I did for
the Evaluate() method.
As you can see, each of the classes needed to be modified to implement
Print(). You can also note that the compiler
complained until each of the classes implemented the Print()
operation. This is because we used an abstract method in the
If you compare the procedural approach vs. the object-oriented approach you
will notice that they are pretty much opposites. It is easy to add operations in
the procedural approach but difficult using an object-oriented approach. Adding
data types is difficult using in a procedural approach, but easy in an
object-oriented approach. Data needs to be public in procedural, but can be
private in object oriented. When deciding which approach to use you need to try
an predict what will occur more often, adding new data types or adding new
operations. What I will present next are some compromise solutions that balance
the advantages and disadvantages.
All source code (text in a code HTML element or text in a element with
the a code class attribute) is available under the
Apache 2.0 licence.
All code (as described above) written prior to 2015 is also
available under the MS-PL license.