Here's a sketch of the Fleet class:
public class Fleet {
private int cap = 100;
private int size = 0;
private Aircraft[] members = new
Aircraft[cap];
public void add(Aircraft a) {
if (size < cap) {
members[size++] = a;
}
}
public void takeoff() {...}
public void fly() {...}
public void land() {...}
public static void main(String[] args)
{
Fleet united = new Fleet();
united.add(new Aircraft());
united.add(new Airplane());
united.add(new Helicopter());
united.add(new Blimp());
united.takeoff();
System.out.println("+++++");
united.land();
}
}
Fleet is implemented in Fleet.java.
Here is the output of main:
An aircraft is taking off
An airplane is taking off
A helicopter is taking off
A blimp is taking off
+++++
An aircraft is landing
An airplane is landing
A helicopter is landing
A blimp is landing
First notice that although the parameter for the add method has type Aircraft, the actual arguments supplied by main are helicopters, blimps, and airplanes. This is an example of the subsumption rule. Since Airplane, Helicopter, and Blimp are sublcasses of Aircraft, Airplane, Helicopter, and Blimp objects can be stored in variables (and parameters) of type Aircraft.
Let's implement takeoff in the Data-Driven style and land in the Control-Driven style:
public void takeoff() {
for(int i = 0; i < size; i++) {
members[i].takeoff();
}
}
public void land() {
for(int i = 0; i < size; i++) {
if (members[i] instanceof Blimp)
((Blimp)members[i]).land();
else if (members[i] instanceof
Airplane)
((Airplane)members[i]).land();
else if (members[i] instanceof
Helicopter)
((Helicopter)members[i]).land();
else if (members[i] instanceof
Aircraft)
members[i].land();
else
System.err.println("unrecognized
aircraft");
}
}
Both methods work. Certainly the Data-Driven style is much shorter. But there are several other advantages.
Suppose CyberAir wants to add a new type of aircraft into their Aircraft hierarchy:
class Saucer extends Aircraft {
void takeoff() {
System.out.println("A flying
saucer is taking off");
}
void takeoff() {
System.out.println("A flying
saucer is flying");
}
void takeoff() {
System.out.println("A flying
saucer is landing");
}
}
The control-driven programmer must now add a line to Fleet.land():
else if (members[i] instanceof Saucer)
((Saucer)members[i]).land();
There are two problems with this. First, Fleet is a client of the Aircraft hierarchy. Our design requires a modification of the client code each time the Aircraft hierarchy is extended. What if the client code isn't available, or what if it is mission-critical code found in an air traffic control tower. Second, how many places in the client code must be modified? How many places did the client assume there were only three types of aircraft? This multi-way conditional could occur hundreds of times in the client code. If we forget to modify just one of these occurrences, then we will have introduced a bug into the client's code!
On the other hand, note that no changes to Fleet.takeoff() are required. In a sense, Fleet.takeoff() is dumber than Fleet.land() because Fleet.takeoff() knows less about the different types of aircraft than Fleet.land() does. But this is one situation where dumber is better. Any time new data is added to the system, smart methods need to be updated. Dumb methods don't.