Features


Pricing A Meal: An Object-Oriented Example In C++

Charles Havener


Charlie Havener is a senior principal engineer at GenRad Inc., where he specializes in test languages for automatic printed cirucuit board test equipment. He is also a C++ instructor in the Northeastern University State-of-the-Art Program. Charlie has masters degrees in electrical engineering from Cornell University and in computer science from Boston University. He may be contacted at (508) 369-4400 extension 3302.

The essence of Object-Oriented Programming (OOP) is that it can often provide a model of the problem domain in software. OOP's direct modeling ability simplifies design and provides a solid framework on which to graft the features that end-users inevitably demand for software products. Naturally, implementing object-oriented designs (OODs) is much easier in a language that directly supports the three primary characteristics of OOP: inheritance, dynamic binding, and data abstraction. This article focuses on these characteristics in three stages, each a short listing, while implementing a program to price a meal. Only a little knowledge of C+ + is assumed.

Suppose a cafeteria has just installed a new PC-based cash register system, and the cafeteria manager has chosen us to create the software. Initially, the system needs only to price the meals, but eventually must also provide weight conscious customers with the calories in the meal, and do inventory control. The system must be flexible since the cafeteria often changes prices and sometimes offers global discounts, on all desserts for example.

Suppose also that our team chooses to develop the system in C++. C++ not only supports OOP via the three main characteristics — inheritance, dynamic binding, and data abstraction — but also allows the programmers to overload operators, such as +, and create user-defined data types such as complex, or String. However, object-oriented modeling problems do not require overloaded operators.

An object-oriented design begins by defining the objects in the problem domain. In the cafeteria domain, an obvious object is the meal itself. The meal is composed of other objects, an Appetizer, an Entree, and a Dessert. To facilitate the "structuring" of objects, C++ supports two kinds of inheritance: the isA, kindOf, or subtype inheritance via the class derivation mechanism; and the partOf or composition inheritance via "layering" objects, one inside another.

Listing 1 contains our first prototype. (The code should work with any C++ 2.0 compiler, including Zortech C++ for PCs. Copy the Zortech stream.hpp to stream.h and string.h to strings.h to maximize portability of code to other compilers). The objects are represented by classes, which are like C structs that can also contain functions. The Meal class contains three pieces of data, a, e, and d that represent the three parts of the meal. An integer data member in the Dessert class remembers the particular kind of dessert when the Dessert object is instantiated. The same mechanism is used for Appetizer and Entree. The naming convention used by most C++ programmers requires that the first letter of a class name be capitalized.

The main() function shows how to create and price the meal.

Meal m(Melon,Fish,Jello);
declares m to be of type Meal. A member function of the class which has the same name as the class is called a constructor. The compiler invokes the constructor implicitly when the program declares an object of the class type. The Meal constructor is passed the enumerated data types, Melon etc., for the parts of the meal. A strongly-typed language like C++ provides protection against inadvertent, though not devious, coding mistakes. For example, you cannot assign an integer (except zero) to a pointer variable. You cannot invoke Meal x(0,1,2) because the integers are not the same type as the new types we declared, i.e., ENTREE, DESSERT, and APPETIZER.

The constructor for Meal passes the data along to the constructors for the Appetizer, Entree, and Dessert objects via the weird syntax of the member initialization list

: a(aval), e(eval), d(dval)
The actual body of the Meal constructor does nothing { }. The old style printf (which is not type safe) prints the meal cost.

m.cost();
invokes the cost function on the object m. In other words, m.cost() sends the cost message to object m. In all of the listings, the function bodies that are defined within the class declaration are expanded inline wherever they are used. Inline functions should be small. They are usually used just for private data access.

At this point, a little thought shows that we must implement a case statement in each Class to properly compute the cost. That is, depending on the kind of dessert ordered, the total cost will be different. The cost() function in the Meal class simply invokes the others, i.e., a.cost()+ e.cost() + d.cost() to obtain the total cost. The little red flag should go up! This is not a good OOD. Whenever you would use a case statement in the class implementation, something is probably wrong. As my OOP instructor often said, "narrow and deep, narrow and deep". The class hierarchy is not deep enough. We need more objects, one for each kind of Appetizer, etc.

A Second Implementation

Listing 2 shows a much improved implementation. Diagram 1 shows the class hierarchy. The Appetizer, Entree and Dessert classes have been made into abstract classes, i.e., ones that are not meant to be instantiated, just inherited from. The isA or kindOf inheritance is shown as a tree with abstract classes as dashed boxes. To avoid arguments about which way the arrows should point on the lines, I just leave them off. The partOf inheritance is effectively portrayed by drawing the boxes for the class objects inside the Meal object box.

To prevent overcrowding the name space, I add _obj to the publicly derived subclasses so that the enumerated names (Fish, Jello, etc.) can stay the same. (If the derivation were private, the derived classes would not be considered a kind — Of the base class. Private derivations are used when the goal is merely code reuse.)

Another major change in the second implementation is that the Meal object contains pointers to the component parts, rather than containing the component parts themselves. In general, using pointers makes the dynamic binding of functions at runtime in C++ work best. Note that you use the component parts in main() the same as before, even though the underlying implementation has been changed radically. The Meal constructor no longer passes data back to embedded object constructors via the member init list. Instead, the new operator creates the constituent part objects. The new operator allocates space for the object, much like C's malloc() function. However, new is type safe and automatically invokes the constructor, if any, for a new object. Constructors ensure that new data objects are initialized properly. In this example, the objects don't contain data so nothing requires explicit initialization.

The Meal class also has a destructor now, ~Meal(). The destructor cleans up the space allocated by the new operator when the meal object goes out of scope. In this example ~Meal() is invoked when the program returns from main().

Runtime Binding

An abstract class serves as a central location for common data or functions used by the classes derived from it. If in the abstract base class we declare cost() as virtual, the compiler arranges for runtime binding to occur whenever that function is invoked on a pointer to the base class. For example, if p is a pointer to dessert, then p->cost() will access the correct dessert cost function. In other words, if you have

p = new Pie_obj;
then p->cost() will be 1.50. If the derived class omits the cost() function, then the base class cost() function is used.

Each class with virtual functions actually contains a pointer to a table of function pointers. When we write p->cost(), the compiler generates something like p->vtbl[1]. Since every derived class has its own vtbl (virtual table), the correct function is invoked (see Diagram 2) . A function that is virtual in the base class is always virtual, even if the derived classes leave off the virtual keyword.

A strongly typed language like C++ generally doesn't let you assign a pointer of one type to a pointer of another type. For public class derivations there is an important exception: you may assign to a variable declared to be a pointer to a base type, a pointer to any type that was publicly derived from that base type.

Unlike many OOP languages, C++ ensures at compile time that if you invoke the cost() function on a dessert object, that function will exist. If you don't redeclare the function in a derived class, then the base class stopper function is invoked. Is there a better way? Yes, pure abstract classes.

Listing 3 shows the final prototype. C++ 2.0 allows virtual function declarations to be followed by = 0;. This somewhat strange syntax means every derived class must implement the function or again define it as pure virtual. The compiler checks the declarations and issues an error message if this is not so. The compiler check ensures that we don't inadvertently leave out the cost() function.

This third version adds some new features. This week all desserts are 25% off except for Pies. By adding a virtual discount() function to the Dessert abstract class we automatically, via inheritance, apply it to all derived desserts. The Meal class's cost function was changed to multiply the dessert cost (d->cost()) by the discount (d->discount ()).

Since pies are the only desserts that have no discount, a discount() multiplier of 1.0 is placed in the Pie_obj class. Every object has a text() function to print information about itself. The Meal::print() member function in Listing 3 uses the standard stream I/O, which is type safe, instead of print f(). In general, using cout, cin and cerr via the overloaded insertion << and extraction >> operators is safer than printf() and you can easily overload the operators for new class types you create; printf() only knows about built-in types like double and char *. However, overloading operators is material for another article. [Ed. note: see David Clark's "A Date Object In C++" in the June 1990 C Users Journal for an example of operator overloading.]

The final design is clean and elegant. The class framework models the real world problem domain directly. Adding calorie information would be much like cost. The class hierarchy actually needs to be deepened further to represent different kinds of cakes and pies. Instead of an item's cost residing in the object, the cost() member function could access a database to fetch the most recent price increase. This short example demonstrates just some of what OOP (and C++ in particular) has to offer.