A student asked the following in the newsgroup:
Just had a quick question about project one. If we can't use if statements or loops then how do we check to see if the operator button pushed has a higher presedence [sic] or not?
My reply follows below. I have enhanced this web-version with color. The color shows the following.
Good question!
The basic question is how to make decisions without using an if statement. The answer lies in proper application of polymorphism.
Let me use the state pattern example from lecture today as a basis for my explanation.
The example in class is based on the idea that a list can be in one of two states, empty or non-empty. The operations of the list function differently in the two states. One way of accomplishing this is to write the code for the list class as follows:
class List
{
boolean isEmpty() {...}
void op_1()
{
if ( this.isEmpty() )
{ /* CODE FOR CASE OF EMPTY LIST */ }
else
{ /* CODE FOR CASE OF NON-EMPTY LIST */ }
}
void op_2()
{
if ( this.isEmpty() )
{ /* CODE FOR CASE OF EMPTY LIST */ }
else
{ /* CODE FOR CASE OF NON-EMPTY LIST */ }
}
.
.
.
void op_N()
{
if ( this.isEmpty() )
{ /* CODE FOR CASE OF EMPTY LIST */ }
else
{ /* CODE FOR CASE OF NON-EMPTY LIST */ }
}
}
Note that every time a new operation is defined, a test of the current state must be carried out. Nothing forces the programmer to perform this test - the compiler can't enforce the writing of the if-else statement in every relevant method.
It is also difficult to see how many states are involved. Perhaps there are only two, perhaps more (maybe most methods only need to make a distinction between two, but perhaps one or two need to make a three-way distinction). Only careful inspection of the code will reveal this.
Reflect also on how difficult it would be to add a new state. We would have to modify every method with a nested if (or switch if we're lucky) and devise some way to distinguish the different states from each other.
Finally, notice that the code for how the list behaves when it's empty is interspersed with code for how the list behaves when it is non-empty. This makes it difficult to see how the list behaves in either state.
Consider the alternative: using polymorphic dispatch to do the selection of state for us. The code ends up being organized differently.
The list has a reference to a state object. The variable is declared to be of some interface (or abstract class) type. The states are defined as classes implementing that interface. The interface specifies all of the operations which must be supported in all states. The compiler can enforce that all methods be defined in the implementing state classes - we can't forget to define a method for a particular state.
It is easy to see how many states are involved: each state is its own class. It is easy to add a new state: no existing code needs to be modified to make the polymorphic dispatch work. The code for each state is gathered together in one place, the state class definition.
Also note that we no longer must remember to write the explicit test to see what state the list is in. Polymorphic dispatch handles this for us (less work for us to do, less chance of forgetting to check the state). If we add a new state to the mix the compiler forces us to define all the required methods, and polymorphic dispatch will continue to dispatch into the correct code when methods are called.
What we have is a code structure of the following form:
class List
{
IState _state;
void op_1() { _state.op_1(); }
void op_2() { _state.op_2(); }
...
void op_N() { _state.op_N(); }
}
class EmptyState implements IState
{
void op_1() { /* CODE FOR EMPTY STATE */ }
void op_2() { /* CODE FOR EMPTY STATE */ }
...
void op_N() { /* CODE FOR EMPTY STATE */ }
}
class NonEmptyState implements IState
{
void op_1() { /* CODE FOR NON-EMPTY STATE */ }
void op_2() { /* CODE FOR NON-EMPTY STATE */ }
...
void op_N() { /* CODE FOR NON-EMPTY STATE */ }
}
You can use this same principle of polymorphic dispatch to do selection of what code to execute based on the type of an object instead of doing selection using an if-statement.
Note that I am not saying that using if-statements is always a bad thing. Sometimes an if-statement is clearer and neater, and sometimes you have to use if-statements (e.g. when working with primitives).
The reason I am not permitting you to use them to solve this project is to force you to conceptualize a solution to the problem in a particular way. Just getting a four-function calculator to work is not much of a challenge for you at this point. Thinking about how to apply polymorphism and design patterns in a solution should hopefully get you thinking, and back into the swing of OO analysis and design.