13. Polymorphism
Part of
CS:2820 Object Oriented Software Development Notes, Spring 2019
|
The code we produced last Monday works, but it is vastly oversimplified. A major detail in real road networks is that there are many types of intersections. We have at least the following variants:
Stop lights have several characteristics, but one of the most significant is that the simplest ones turn green in two directions while they are red in the other two directions. This means that, for example, the lights facing both north and south are green when the east and west lights are red, and visa versa. More complex stoplights have turn arrows, but for all varieties of stoplights, roads into or out of that intersection must have labels indicating the direction from which they enter or leave.
Similarly, in the neural net example, we have several kinds of synapses. There are excitatory synapses where an action potential traveling down an axon to that synapse causes a positive change in the receiving neuron, pushing it closer to the threshold that would cause it to fire, and there are inhibitory synapses that cause a negative change in the receiving neuron, making it less likely to fire. there are also axosynaptic interfaces where a secondary synapse transmits signals to a primary synapse, activating or inhibiting the primary synapse.
In a logic simulator, there are several kinds of gates. We typically speak of and, or, and not gates, but there are also nand, nor, and exclusive-or gates, as well as assymetric gates that perform functions such as a and not b. This means that we must document each wire leading to a gate by indicating which input it connects to. In the general case, gates may have multiple outputs, so wires from a gate must also be tagges with which output they connect to.
This has an immediate impact on our road network description. Where we formerly just said:
intersection A intersection B
We can now say things like:
intersection A stoplight intersection B
We've made a decision above, a decision that has several consequences. That is, for specialized types of intersection, we explicitly name the intersection type, but there is also a default where there is no explicit name. We could have reqired intersection B above to be declared as a simple intersection or something like that. The primary problem with this design decision is that it complicates the problem of parsing the input file by forcing us to recognize the end of line.
A second consequence is that for roads, we need to document how the road connects to the intersections it joins, for example, using a notation like this:
road A north B south
This means that there is a road leaving intersection A going north to intersection B where it enters from the south.
Java supports polymorphic classes, that is, classes where there are multiple possible implementations. In fact, polymorphism was added to Simula '67, the original object-oriented programming language, precisely to allow for the kind of variation discussed above.
Specifically Java, C++ and their ancestor Simula '67 all allow us to introduce new classes that extend an existing class. For example, in Java we could have:
/** Intersections with no stop light
* @see Intersection
*/
class NoStop extends Intersection {
}
/** Intersections with a stop light
* @see Intersection
*/
class StopLight extends Intersection {
}
How do these new subclasses differ from the parent class? The simplest place they differ is in the toString method, so we can immediately create new methods for that:
/** Intersections with no stop light
* @see Intersection
*/
class NoStop extends Intersection {
String toString() {
return (
"intersection " +
name
);
}
}
/** Intersections with a stop light
* @see Intersection
*/
class StopLight extends Intersection {
String toString() {
return (
"intersection " +
name +
" stoplight"
);
}
}
In the above, we've made the output of the toString() method recreate our input text, unless the input contained tabs or multiple spaces between the words. Information about those details is lost by the scanner we are using.
Note that wherever it is legal to have an Intersection, it is now legal to have either a NoStop or a StopLight. Consider the following declarations in a hypothetical bit of code:
Intersection i; NoStop n; StopLight s;
When you create a new object, you must pick its actual class. Once an object is created, you cannot change its class. So, we must change the code to initialize intersections. Here is the old code for readNetwork()
static void readNetwork( Scanner sc ) {
while (sc.hasNext()) {
// until the input file is finished
String command = sc.next();
if ("intersection".equals( command ))
inters.add( new Intersection( sc, inters ) );
} else if ("road".equals( command ))
roads.add( new Road( sc, inters ) );
} else {
// Bug: Should probably support comments
Errors.warning(
"'"
+ command
+ "' not a road or intersection"
);
// Bug: Should skip to the next line
}
}
}
We need to change the part for initializing intersections. This raises a problem. Our input language is arranged like this:
intersection X stoplight
Here is an alternative syntax:
intersection stoplight X
If we had done the latter, we could have learned that we were about to initialize a member of class StopLight before we knew its name, so we could call the correct constructor and let the constructor pick up the name, as it does in our current version of the code. Now, however, we must scan over the name first before we discover the type. One way to do this would be as follows:
String command = sc.next();
if ("intersection".equals( command ))
// Bug: What if no sc.next?
String name = sc.next();
// Bug: What if no sc.next?
String kind = sc.next();
if (kind == "stoplight") {
inters.add( new StopLight( sc, name ) );
} else {
// Bug: How do we undo sc.next()?
inters.add( new NoStop( sc, name ) );
}
} else if ("road".equals( command ))
...
This solution would force readNetwork() to know about every subclass of Intersection. As a general rule, one way to improve the maintainability of large programs is to limit the need for one part of the program to know anything about internal details of another part. From the top level, all we need to know is that there are intersections. Only within class Intersection is there any reason to know that there are subclasses. Therefore, we will abandon this solution.