next up previous
Next: Final version (The Role Up: Hardware Store Previous: 2nd version (Finding a

3rd version (Differences between classes)

Motto: a class should describe a set of objects


We were encountered the similarities between the classes, it is time to answer the question: what makes these classes different? What is the difference between CDRom and Network? The difference is found in the derived class declarations. Neither class adds new members; they have no additional state or supplementary behaviour. The difference is in their definitions of the virtual functions price(), name() and rebate().

These virtual functions do not vary the behaviour of the objects of the different derived classes. The only difference between a Network object and a CDRom object is the values returned by their virtual functions.

In general, the behaviour of an object is the way it responds to each stimulus it can receive. Viewing an object in terms of its response to stimuli emphasizes the independence of objects - each object is an autonomous component of a program in execution.

The most common form of stimulus is a member function call. The object responds by executing its member function, either to perform a side effect or to return a value or both. Polymorphism - virtual functions in C++ - permits the objects of different types (classes) to respond in different ways to the same stimulus. In this program, the virtual functions do not produce variation in behaviour between objects.

The difference between the derived classes can be seen from another perspective. All information in an object of one of these leaf classes is incorporated in its type. A Tape object, for example has no data members, every Tape object is equivalent to every other. There is no reason to instantiate more than one Tape object, because they must all behave in the very same way. Such a type is called Singleton, and sometimes is an essential part of a program. But a program containing only singletons - that is very suspicious.

Generality is an essential property of a program. Code fragments addressed to solve general problems are more usefull than those that are restricted to specific problems. In practice, a program cannot afford to define a different class for every object that it creates. Rather, each class should characterize a set of objects.

#include <iostream>
using namespace std;

enum CARD       {CDROM, TAPE, NETWORK };
enum MONITOR    { MONO, COLOR };

class Component
{
public:
        Component( int p, char *n, int r = 0);
    int     netPrice();
    int     price()  { return m_price; }
    char   *name()   { return m_name; }
    int     rebate() { return m_rebate; }
private:
    int     m_price;
    char   *m_name;
    int     m_rebate;
};

Component::Component( int p, char *n, int r)
                : m_price(p), m_name(n), m_rebate(r)  { } 

int   Component::netPrice()
{
    return price() - rebate();
}

The 1st version of the program was drawn into the common trap of thinking that inheritance and virtual functions are the only ways to program in C++. The excessive use of inheritance resulted in class declarations in which some classes are so specialized that each describes just one object. Inheritance and polymorphism are powerful tools when behavious varies between objects of different classes. However, in this program the variation is in values, not behaviour.

Simple data members and non-virtual functions are sufficient to represent the differences between componenct objects.

The difference between components is now a difference in value, not a difference in type. The different values are established by arguments to the constructor.

Use data members for variation in value, reserve virtual functions for variation in behaviour.

Component Network(600, "Network", 45);
Component CDRom(1500, "CDRom", 135);
Component Tape(1000, "Tape", 45);
Component Color(1500, "Color");
Component Monochrome(500, "Mono");

The objects above are automatic ones, in a real program perhaps we create them on user request from the heap.

class Computer
{
public:
            Computer( CARD, MONITOR );
    int     netPrice();
    void    print();
private:
    Component   *card;
    Component   *mon;
};

The Computer has changed accordingly the changes above. It does not create new Components and destroys them. Therefore no destructor is needed anymore.

Computer::Computer( CARD c, MONITOR m )
{
    switch( c )
    {
        case NETWORK:   card = & Network;     break;
        case CDROM:     card = & CDRom;       break;
        case TAPE:      card = & Tape;        break;
    }
    switch( m )
    {
        case MONO:      mon = & Monochrome;   break;
        case COLOR:     mon = & Color;        break;
    }
}

Unification of the base abstractions (creating the common base class Component permits a simplification of the pricing. Both Card and Monitor have a rebate, so we can implement netPrice() in Component:

As a consequece we can drastically simplify the netPrice() of Computer. It is the sum of netPrice()-es of the Components.

int   Computer::netPrice()
{
    return mon->netPrice() + card->netPrice();
}

void    Computer::print()
{
    cout << "Configuration = " << card->name() 
         << ", "               << mon->name()  
         << ", net price = "   << netPrice() 
         << endl;
}

See the client code: no modification was needed here.

int main()
{
    Computer nm( NETWORK, MONO );
    Computer cm( CDROM,   MONO );
    Computer tm( TAPE,    MONO );
    Computer nc( NETWORK, COLOR );
    Computer cc( CDROM,   COLOR );
    Computer tc( TAPE,    COLOR );

    nm.print();
    cm.print();
    tm.print();
    nc.print();
    cc.print();
    tc.print();

    return 0;
}


next up previous
Next: Final version (The Role Up: Hardware Store Previous: 2nd version (Finding a
Porkoláb Zoltán 2001-09-03