Creating class hierarchies is an extension of the first step, identifying classes, but it requires information gained during the second and third steps. By assigning attributes and behavior to classes, you have a clearer idea of their similarities and differences; by identifying the relationships between classes, you see which classes need to incorporate the functionality of others.
One indication that a hierarchy might be appropriate is the use of a switch statement on an object's type. For example, you might design an Account class with a data member whose value determines whether the account is a checking or savings account. With such a design, the class's member functions might perform different actions depending on the type of the account:
class Account
{
public:
int withdraw( int amount );
// ...
private:
int accountType;
// ...
};
int Account::withdraw( int amount )
{
switch ( accountType )
{
case CHECKING:
// perform checking-specific processing
break;
case SAVINGS:
// perform savings-specific processing
break;
// ...
};
}
A switch statement like this usually means that a class hierarchy with polymorphism is appropriate. As described in Chapter 7, polymorphism lets you call member functions for an object without specifying its exact type, by using virtual functions.
In the example above, the Account class can be made into an abstract base class with two derived classes, Savings and Checking. The withdraw function can be declared virtual, and the two derived classes can each implement their own version of it. Then you can call withdraw for an object without examining the object's precise type.
On the other hand, a hierarchy isn't needed just because you can identify different categories of a class. For example, is it necessary to have Sedan and Van as derived classes of Car? If you perform the same processing for every type of car, then a hierarchy is unnecessary. In this case, a data member is appropriate for storing the type of car.
Both composition and inheritance enable a class to reuse the code of another class, but they imply different relationships. Many programmers automatically use inheritance whenever they want to borrow the functionality of an existing class, without considering whether inheritance accurately describes the relationship between their new class and the existing one. Composition should be used when one class has another class, while inheritance should be used when one class is a kind of another class. For example, a circle is not a kind of point; it has a point. Conversely, a numeric data field does not contain a generic data field; it is a data field.
Sometimes it is difficult to determine whether inheritance or composition is appropriate. For example, is a stack a kind of list with a special set of operations, or does a stack contain a list? Is a window a kind of text buffer that can display itself, or does a window contain a text buffer? In such cases, you have to examine how the class fits in with the other classes in your design.
One indication that inheritance is the appropriate relationship is when you want to use polymorphism. With inheritance, you can refer to a derived object with a pointer to its base class and call virtual functions for it. With composition, however, you cannot refer to a composite object with a pointer to one of its member's classes, and you cannot call virtual functions.
If you want to borrow another class's functionality more than once, composition is probably more appropriate. For example, if you're writing a FileStamp class and you want each object to store a file's creation date, last modification date, and last read date, composition is clearly preferable to inheritance. Rather than use a complicated multiple inheritance design, it's much simpler to include three Date objects as members.
Designing Classes for Inheritance
Building class hierarchies usually involves creating new classes as well as modifying or discarding ones previously identified. Most of the classes identified during the first step are probably ones that you intend to instantiate. However, when the common features of several classes are isolated, they often don't describe a class that is useful to instantiate. As a result, the new classes you create when forming a hierarchy may be abstract classes, which are not meant to have instances.
Adding abstract classes increases the ability to reuse a class. For example, you might create one abstract class that five classes inherit from directly. However, if two of those deriving classes share features that the others don't, those features can't be placed in the base class. As a result, they would have to be implemented in each class they appeared in. To prevent this, you can create an intermediate abstract class that is itself derived from the base, but adds new features. The two classes can inherit their shared characteristics from this class. This also gives you greater flexibility when extending the hierarchy later on.
However, abstract classes should not be created indiscriminately. As an extreme example, it's possible to create a series of abstract classes, each of which inherits from another and adds only one new function. While in theory this promotes reusability, in practice it creates a very clumsy hierarchy.
It is desirable to place common features as high in a hierarchy as possible to maximize their reuse. On the other hand, you should not burden a base class with features that few derived classes will use. Attributes and behavior may be shifted up and down the hierarchy as you decide which features should be placed in a base class.
Inheritance not only affects the design of class hierarchies, it can also affect the design of classes that stand alone. Any class you write might later be used as a base class by another programmer. This requires a refinement to the distinction between a class's interface and implementation. A class has two types of clients: classes or functions that use the class; and derived classes that inherit from the class. When designing a class, you must decide whether you want to define different interfaces for these two types of clients. The protected keyword allows you to make members visible to the derived classes but not to the users. You can thus give derived classes more information about your class than you give users.
A protected interface can reveal information about a class's implementation, which violates the principle of encapsulation. Modifying the protected portion of a class means that all derived classes must be modified too. Accordingly, the protected keyword should be used with care.
Multiple inheritance can be useful for maximizing reuse while avoiding base classes with unnecessary functionality. For example, remember the example of the abstract base class SortableObject, which has the interface that a class needs in order to be stored in a SortedList. Now consider a similar abstract base class called PrintableObject, which has an interface that all printable classes can use. You might have some classes that are printable, some that are sortable, and some that are both. Multiple inheritance lets you inherit the properties you need from the abstract base classes.
This scenario is difficult to model using only single inheritance. To avoid
duplicating functions in different classes, you'd have to define a base class
PrintableSortableObject and derive all your other classes from it. Certain classes would have to suppress the functions of the printable interface, while others would have to suppress the functions of the sortable interface. Such a class hierarchy is top-heavy, having too much functionality in its base class.
Virtual base classes are often used in hierarchies built around multiple inheritance. One drawback of virtual base classes, besides the processing overhead they entail, is that the need for them only becomes apparent when you design an entire hierarchy at once. Consider the example of the SalesManager class in Chapter 7. The need to make Employee a virtual base class doesn't arise until SalesManager is defined. If you didn't define SalesManager at the outside, but instead defined it after the other classes have been used in many programs, you must modify the existing hierarchy, causing extensive recompilation. If modifying the hierarchy isn't practical, you must use some other solution instead of virtual base classes.
Just as with single inheritance, multiple inheritance is often used inappropriately. Many situations where multiple inheritance is used are better modeled with composition or with a combination of composition and single inheritance. You should always examine one of these options when considering multiple inheritance.
Now that you've seen the major steps in designing object-oriented programs, the next chapter gives a concrete example of how this design technique can be applied.