Extensibility
With play( ) defined as virtual
in the base class, you can add as many new types as you want without changing
the tune( ) function. In a well-designed OOP program, most or all
of your functions will follow the model of tune( ) and communicate
only with the base-class interface.
Such a program is extensible because you can add
new functionality by inheriting new data types from the common base class. The
functions that manipulate the base-class interface will not need to be changed
at all to accommodate the new classes.
Here’s the instrument example with more
virtual functions and a number of new classes, all of which work correctly with
the old, unchanged tune( ) function:
//: C15:Instrument4.cpp
// Extensibility in OOP
#include <iostream>
using namespace std;
enum note { middleC, Csharp, Cflat }; // Etc.
class Instrument {public:
virtual void play(note) const { cout << "Instrument::play" << endl;
}
virtual char* what() const { return "Instrument";
}
// Assume this will modify the object:
virtual void adjust(int) {}};
class Wind : public Instrument {public:
void play(note) const { cout << "Wind::play" << endl;
}
char* what() const { return "Wind"; } void adjust(int) {}};
class Percussion : public Instrument {public:
void play(note) const { cout << "Percussion::play" << endl;
}
char* what() const { return "Percussion"; } void adjust(int) {}};
class Stringed : public Instrument {public:
void play(note) const { cout << "Stringed::play" << endl;
}
char* what() const { return "Stringed"; } void adjust(int) {}};
class Brass : public Wind {public:
void play(note) const { cout << "Brass::play" << endl;
}
char* what() const { return "Brass"; }};
class Woodwind : public Wind {public:
void play(note) const { cout << "Woodwind::play" << endl;
}
char* what() const { return "Woodwind"; }};
// Identical function from before:
void tune(Instrument& i) { // ...
i.play(middleC);
}
// New function:
void f(Instrument& i) { i.adjust(1); }
// Upcasting during array initialization:
Instrument* A[] = { new Wind,
new Percussion,
new Stringed,
new Brass,
};
int main() { Wind flute;
Percussion drum;
Stringed violin;
Brass flugelhorn;
Woodwind recorder;
tune(flute);
tune(drum);
tune(violin);
tune(flugelhorn);
tune(recorder);
f(flugelhorn);
} ///:~
You can see that another inheritance level
has been added beneath Wind, but the virtual mechanism works
correctly no matter how many levels there are. The adjust( ) function
is not overridden for Brass and Woodwind. When this
happens, the “closest” definition in the inheritance hierarchy is automatically
used – the compiler guarantees there’s always some definition for a
virtual function, so you’ll never end up with a call that doesn’t bind to a
function body. (That would be disastrous.)
The array A[ ] contains pointers to
the base class Instrument, so upcasting occurs during the process of
array initialization. This array and the function f( ) will be used
in later discussions.
In the call to tune( ),
upcasting is performed on each different type of object,
yet the desired behavior always takes place. This can be described as “sending
a message to an object and letting
the object worry about what to do with it.” The virtual function is the
lens to use when you’re trying to analyze a project: Where should the base
classes occur, and how might you want to extend the program? However, even if
you don’t discover the proper base class interfaces and virtual functions at the
initial creation of the program, you’ll often discover them later, even much
later, when you set out to extend or otherwise maintain the program. This is
not an analysis or design error; it simply means you didn’t or couldn’t know
all the information the first time. Because of the tight class modularization
in C++, it isn’t a large problem when this occurs because changes you make in
one part of a system tend not to propagate to other parts of the system as they
do in C.