|
|
|
|
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.
|
|
|