Indometric


Sep 29
Monday
C++

C++ Data Abstraction & Polymorphism

  • Sharebar

C++ Common Knowledge

Data Abstraction

A “type” is a set of operations, and an “abstract data type” is a set of operations with an implementation. When we identify stuff in a problem domain, the first question we must question about them is, “What can I do with this object?” not “How is this object implemented?” Therefore, if a natural description of a problem involves employees, contracts, and payroll records, then the programming language used to solve the problem must contain Employee, Narrow, and PayrollRecord types. This allows an efficient, two-way translation between the problem domain and the solution domain, and software written this way has less “translation noise” and is simpler and more right.

In a general-purpose programming language like C++, we don’t have application-specific types like Employee. Instead, we have something better: the language facilities to start sophisticated abstract data types. The purpose of an abstract data type is, essentially, to extend the programming language into a particular problem domain.

No universally accepted procedure exists for designing abstract data types in C++. This aspect of programming still has its share of inspiration and artistry, but most successful approaches follow a set of similar steps.

  1. Choose a descriptive name for the type. If you have distress choosing a name for the type, you don’t know enough about what you want to implement. Go reckon some more. An abstract data type must speak for a single, well-defined concept, and the name for that concept must be obvious.
  2. List the operations that the type can go. An abstract data type is defined by what you can do with it. Remember initialization (constructors), cleanup (destructor), photocopying (copy operations), and conversions (nonexplicit single-argument constructors and conversion operators). Never, ever, simply grant a bunch of get/set operations on the data members of the implementation. That’s not data abstraction; that’s laziness and lack of imagination.
  3. Design an interface for the type. The type must be, as Scott Meyers tells us, “simple to use correctly and hard to use incorrectly.” An abstract data type extends the language; do proper language design. Place yourself in the place of the user of your type, and write some code with your interface. Proper interface design is as much a question of psychology and empathy as technological competency.
  4. Implement the type. Don’t let the implementation affect the interface of the type. Implement the narrow promised by the type’s interface. Remember that the implementations of most abstract data types will change much more frequently than their interfaces.

Polymorphism

The topic of polymorphism is given mystical status in some programming texts and is ignored in others, but it’s a simple, helpful concept that the C++ language supports. According to the standard, a “polymorphic type” is a class type that has a virtual function. From the design perspective, a “polymorphic object” is an object with more than one type, and a “polymorphic base class” is a base class that is calculated for use by polymorphic stuff.

Deliberate a type of financial option, AmOption, as shown in Figure 1.

Figure 1. Polymorphic leveraging in a financial option hierarchy. An American option has four types.

An AmOption object has four types: It is simultaneously an AmOption, an Option, a Deal, and a Priceable. Because a type is a set of operations, an AmOption object can be manipulated through any one of its four interfaces. This means that an AmOption object can be manipulated by code that is written to the Deal, Priceable, and Option interfaces, thereby allowing the implementation of AmOption to leverage and reuse all that code. For a polymorphic type such as AmOption, the most vital things inherited from its base classes are their interfaces, not their implementations. In fact, it’s not uncommon, and is often desirable, for a base class to consist of nothing but interface.

Of course, here’s a catch. For this leveraging to work, a properly calculated polymorphic class must be substitutable for each of its base classes. In other words, if generic code written to the Option interface gets an AmOption object, that object had better behave like an Option!

This is not to say that an AmOption must behave identically to an Option. (For one thing, it may be the case that many of the Option base class’s operations are pure virtual functions with no implementation.) Very, it’s profitable to reckon of a polymorphic base class like Option as a narrow. The base class makes certain promises to users of its interface; these include firm syntactic promises that certain member functions can be called with certain types of arguments and less easily demonstrable semantic promises concerning what will really recommend itself when a particular member function is called. Concrete derived classes like AmOption and EurOption are subcontractors that implement the narrow Option has established with its clients, as shown in Figure 2.

Figure 2. A polymorphic contractor and its subcontractors. The Option base class specifies a narrow.

For example, if Option has a pure virtual price member function that gives the present value of the Option, both AmOption and EurOption must implement this function. It obviously won’t implement identical behavior for these two types of Option, but it must calculate and return a price, not make a touchtone phone call or print a file.

On the other hand, if I were to call the price function of two different interfaces to the same object, I’d better get the same result. Essentially, either call must bind to the same function:

AmOption *d = new AmOption;
Option *b = d;
d->price(); // if this calls AmOption::price...
b->price(); // ...so must this!

This makes sense. (It’s startling how much of advanced object-oriented programming is basic common sense surrounded by impenetrable syntax.) If I were to question you, “What’s the present value of that American option?” I’d guess to receive the same answer if I’d phrased my question as, “What’s the present value of that option?”

The same reasoning applies, of course, to an object’s nonvirtual functions:

b->update(); // if this calls Option::update...
d->update(); // ...so must this!

The narrow provided by the base class is what allows the “polymorphic” code written to the base class interface to work with specific options while promoting healthful ignorance of their being. In other words, the polymorphic code may be manipulating AmOption and EurOption stuff, but as far as it’s concerned they’re all just Options. Various concrete Option types can be added and removed without affecting the generic code that is aware only of the Option base class. If an AsianOption shows up at some point, the polymorphic code that knows only about Options will be able to manipulate it in blissful ignorance of its specific type, and if it must later disappear, it won’t be missed.

By the same nominal, concrete option types such as AmOption and EurOption need to be aware only of the base classes whose contracts they implement and are independent of changes to the generic code. In principle, the base class can be ignorant of everything but itself. From a practical perspective, the design of its interface will take into account the requirements of its anticipated users, and it must be calculated in such a way that derived classes can easily deduce and implement its narrow. But, a base class must have no specific knowledge of any of the classes derived from it, because such knowledge inevitably makes it hard to add or remove derived classes in the hierarchy.

In object-oriented design, as in life, ignorance is bliss.


Post Tags: ,


Post a Comment

 


All content and source © 2010 Indometric. All rights reserved. See our Privacy Policy and DMCA Information