Inheritance and Dynamic Binding: idioms and common patterns
Steven Zeil
1 Inheritance Captures Variant Behavior
Why bother with inheritance & dynamic binding at all?
Because it offers a convenient mechanism for capturing variant behaviors among different groups of objects.
string render(int width)
Value* clone()
But to actually store a numeric value, we need a data member to hold a number (and a function via which we can retrieve it, though we’ll ignore that for the moment). Similarly, we can expect that, to store a string value, we would need a data member to store the string.
We will assume, therefore, that
-
Numeric values have an attribute
d
of typedouble
-
String values have an attribute
s
of typestring
A pre-OO Approach to Variant behavior
class Value {
public:
enum ValueKind {Numeric, String, Error};
Value (double dv);
Value (string sv);
Value ();
ValueKind kind;
double d;
string s;
string render(int width) const;
}
- Any given value will presumably have something useful stored in
d
or ins
, but not in both.
A pre-OO Approach to Variant behavior
class Value {
public:
enum ValueKind {Numeric, String, Error};
Value (double dv);
Value (string sv);
Value ();
ValueKind kind;
double d;
string s;
string render(int width) const;
}
kind
is a “control” data field.- It does not actually store useful data of its own.
- It’s there to tell us which of the variants of value we have stored in any particular value.
- We’ll use this mainly so that we can branch to code appropriate to that variant.
Multi-Way Branching
string Value::render(int width) const {
switch (kind) {
case Numeric: {
string buffer;
ostringstream out (buffer);
out << dv.d;
return buffer.substr(0,width);
}
case String:
return sv.s.substr(0.width);
case Error:
return string("** error **").substr(0.width);
}
}
- We can expect to see similar multi-way branches used to implement
clone()
or just about every other function we might write for manipulatingValue
s.
Variant Behavior under OO
class Value {
public:
virtual string render(int width) const;
};
class NumericValue: public Value {
public:
NumericValue (double dv);
double d;
string render(int width) const;
};
class StringValue: public Value {
public:
StringValue (string sv);
string s;
string render(int width) const;
};
class ErrorValue: public Value {
ErrorValue ();
string render(int width) const;
};
Now, compare that with the OO approach.
-
We represent each variant with a distinct subclass.
- Only objects of the
NumericValue
class get thed
data member. - Only objects of the
StringValue
class get thes
data member. - None of them get the
kind
data member.
- Only objects of the
-
This saves memory when we have large numbers of values floating about (as in a very large spreadsheet).
-
But what’s more important is how it affects the code we write for manipulating
Value
s.
Variants are Separated
string NumericValue::render(int width) const
{
string buffer;
ostringstream out (buffer);
out << d;
return buffer.substr(0,width);
}
string StringValue::render(int width) const {
return s.substr(0.width);
}
string ErrorValue::render(int width) const {
return string("** error **").substr(0.width);
}
Here’s the OO take on the same render
function.
-
None of the details of how to render specific kinds of value have been changed.
-
But we have repackaged that code into subclass-specific bodies.
-
The “variants” are now separate. In a team environment, different people can work on different variants separately.
-
Each subclass operation is simpler.
-
Most important of all, new kinds of values can be added without changing or recompiling the code of the earlier kinds of values.
-
Summary
We use inheritance in our programming designs whenever we find ourselves looking at objects that
-
come in different varieties,
-
each of which has its own slightly different way of doing the “same thing”.
Conversely, if we don’t see that kind of variant behavior, we probably have no need for inheritance.
- Should we have, for example, a subclass for
AlphaNumericStringValues
?- Unlikely – I can’t think of a way in which the behavior of an alphanumeric string would vary, in the spreadsheet world, from that or an ordinary string.
2 Using Inheritance
We’ll look at 3 idioms describing good ways to use inheritance.
-
Specialization
-
Specification
-
Extension
2.1 Specialization
When inheritance is used for specialization,
-
The new class is a specialized form of the parent class
-
but satisfies the specification of the parent in every respect.
The new class may therefore be substituted for a value of the parent.
This is, in many ways, the “classical” view of inheritance.
Recognizing Specialization
-
A hallmark of specialization is that the base class
-
has objects of its own
-
may be processed in some applications without any contributions from the subclasses.
2.2 Specification
Inheritance for specification
takes place when
-
a parent class specifies a common interface for all children
-
but does not itself implement the behavior
-
Sometimes called the “shared protocol” pattern
Defining Protocols
A protocol is a set of messages (functions) that can be used together to accomplish some desired task.
-
The superclass defines the protocol.
-
The subclasses implement the messages of the protocol in their own manner.
-
Application code invokes the messages of the protocol without worrying about the individual methods.
Recognizing the Specification Idiom of Inheritance
-
Base class typically has no instances (objects)
-
Only objects are actually instances of subclasses
-
Some operations may not even be implementable in the general base class
Example: Varying Data Structures
A common requirement in many libraries is to provide different data structures for the same abstraction.
-
Allows application code writers to balance speed and storage requirements against expected size and usage patterns.
-
Allows code in which the only mention of which data structure is being used comes when the actual objects are constructed
libg++
-
AVLSet
stored the data in AVL trees (a balanced binary tree) -
BSTSet
stored the data in ordinary binary search trees -
CHSet
stored the data in a conventional hash table -
SLSet
stored the data in a singly-linked list
Working with a Specialized Protocol
void generateSet (int numElements, Set& s, int *expected)
{
int elements[MaxSetElement];
for (int i = 0; i < MaxSetElement; i++)
{
elements[i] = i;
expected[i] = 0;
}
// Now scramble the ordering of the elements array
for (i = 0; i < MaxSetElement; i++)
{
int j = rand(MaxSetElement);
int t = elements[i];
elements[i] = elements[j];
elements[j] = t;
}
// Insert the first numElements values into s
s.clear();
for (i = 0; i < numElements; i++)
{
s.add(elements[i]);
expected[elements[i]] = 1;
}
}
A programmer could write code, like the code shown here, that could work an any set.
- It is only in the code that declared a new set variable that an actual choice would have to be made.
2.2.1 Abstract Base Classes
Adding to a Set Subclass
If we are working with libg++
This is OK:
void foo (Set& s, int x)
{
s.add(x);
cout << x << " has been added." << endl;
}
int main ()
{
BSTSet s;
foo (s, 23);
⋮
Adding to a General Set
But what should happen here?
void foo (Set& s, int x)
{
s.add(x);
cout << x << " has been added." << endl;
}
int main ()
{
Set s;
foo (s, 23);
⋮
-
add()
makes no sense ifSet
doesn’t have a data structure that can actually store elements. -
In fact,
Set s;
makes no sense.
How Do We Prevent This?
void foo (Set& s, int x)
{
s.add(x);
cout << x << " has been added." << endl;
}
int main ()
{
Set s;
foo (s, 23);
⋮
-
We could add a method so
Set()
that prints an error message whenadd()
is called. -
Better is to force a proper choice of data structure at compile time.
Abstract Member Functions
class Set {
⋮
virtual Set& add (int) = 0;
⋮
};
-
The
= 0
indicates that no method exists in this class for implementing this message. -
add is called an abstract member function.
-
Subclasses must provide the actual methods (bodies) for these functions.
-
Abstract Classes
An abstract class in C++ is any class that
-
contains an
= 0
annotation on a member function, or -
inherits such a function and does not provide a method for it.
“Abstract classes” are also known as pure virtual classes.
Set as an Abstract Class
Set
in libg++
is a good example of a class that should be abstract.
- We can’t possibly implement
Set::Add
- we need to do that in its subclasses.
Limitations of Abstract Classes
Abstract classes carry some limitations, designed to make sure we use them in a safe manner.
class Set {
⋮
virtual Set& add (int) = 0;
⋮
};
⋮
void foo (Set& s, int x) // OK
⋮
int main () {
Set s; // error!
foo (s, 23);
⋮
-
You cannot construct an object whose type is an abstract class.
-
You cannot declare function parameters of an abstract class type when passing parameters “by copy”.
-
but you can pass pointers/references to the abstract class type.
-
Abstract Classes & Specification
-
Base classes in inheritance-for-specification are often abstract
-
Base class exists to define a protocol
-
Not to provide actual objects
#ifndef VALUE_H
#define VALUE_H
#include <string>
#include <typeinfo>
//
// Represents a value that might be obtained for some spreadsheet cell
// when its formula was evaluated.
//
// Values may come in many forms. At the very least, we can expect that
// our spreadsheet will support numeric and string values, and will
// probably need an "error" or "invalid" value type as well. Later we may
// want to add addiitonal value kinds, such as currency or dates.
//
class Value
{
public:
virtual ~Value() {}
virtual std::string render (unsigned maxWidth) const = 0;
// Produce a string denoting this value such that the
// string's length() <= maxWidth (assuming maxWidth > 0)
// If maxWidth==0, then the output string may be arbitrarily long.
// This function is intended to supply the text for display in the
// cells of a spreadsheet.
virtual Value* clone() const = 0;
// make a copy of this value
protected:
virtual bool isEqual (const Value& v) const = 0;
//pre: typeid(*this) == typeid(v)
// Returns true iff this value is equal to v, using a comparison
// appropriate to the kind of value.
friend bool operator== (const Value&, const Value&);
};
inline
bool operator== (const Value& left, const Value& right)
{
return (typeid(left) == typeid(right))
&& left.isEqual(right);
}
#endif
-
Our spreadsheet Value class is abstract.
-
So is our spreadsheet Expression class.
#ifndef EXPRESSION_H
#define EXPRESSION_H
#include <string>
#include <iostream>
#include "cellnameseq.h"
class SpreadSheet;
class Value;
// Expressions can be thought of as trees. Each non-leaf node of the tree
// contains an operator, and the children of that node are the subexpressions
// (operands) that the operator operates upon. Constants, cell references,
// and the like form the leaves of the tree.
//
// For example, the expression (a2 + 2) * c26 is equivalent to the tree:
//
// *
// / \
// + c26
// / \
// a2 2
class Expression
{
public:
virtual ~Expression() {}
// How many operands does this expression node have?
virtual unsigned arity() const = 0;
// Get the k_th operand
virtual const Expression* operand(unsigned k) const = 0;
//pre: k < arity()
// Evaluate this expression
virtual Value* evaluate(const SpreadSheet&) const = 0;
// Copy this expression (deep copy), altering any cell references
// by the indicated offsets except where the row or column is "fixed"
// by a preceding $. E.g., if e is 2*D4+C$2/$A$1, then
// e.copy(1,2) is 2*E6+D$2/$A$1, e.copy(-1,4) is 2*C8+B$2/$A$1
virtual Expression* clone (int colOffset, int rowOffset) const = 0;
virtual CellNameSequence collectReferences() const;
static Expression* get (std::istream& in, char terminator);
static Expression* get (const std::string& in);
virtual void put (std::ostream& out) const;
// The following control how the expression gets printed by
// the default implementation of put(ostream&)
virtual bool isInline() const = 0;
// if false, print as functionName(comma-separated-list)
// if true, print in inline form
virtual int precedence() const = 0;
// Parentheses are placed around an expression whenever its precedence
// is lower than the precedence of an operator (expression) applied to it.
// E.g., * has higher precedence than +, so we print 3*(a1+1) but not
// (3*a1)+1
virtual string getOperator() const = 0;
// Returns the name of the operator for printing purposes.
// For constants, this is the string version of the constant value.
};
inline std::istream& operator>> (std::istream& in, Expression*& e)
{
string line;
getline(in, line);
e = Expression::get (line);
return in;
}
inline std::ostream& operator<< (std::ostream& out, const Expression& e)
{
e.put (out);
return out;
}
inline std::ostream& operator<< (std::ostream& out, const Expression* e)
{
e->put (out);
return out;
}
#endif
2.3 Extension
In this style of inheritance, a limited number of “new” abilities is grafted onto an otherwise unchanged superclass.
class FlashingString: public StringValue {
bool _flash;
public:
FlashingString (std::string);
void flash();
void stopFlashing();
};
Are Extensions OK?
-
Extensions are often criticized as “hacks” reflecting afterthoughts & poor hierarchy designs.
-
There is often a cleaner way to achieve the same design
-
A “socially acceptable” form of extension is the mixin
Mixins
A mixin is a class that makes little sense by itself, but provides a specialized capability when used as a base class.
- Mixins in C++ often wind up involving multiple inheritance.
Mixin Example: Noisy
Noisy is a mixin I use when debugging.
#ifndef NOISY_H
#define NOISY_H
class Noisy
{
static int lastID;
int id;
public:
Noisy();
Noisy(const Noisy&);
virtual ~Noisy();
void operator= (const Noisy&);
};
#endif
#include "noisy.h"
#include <iostream>
using namespace std;
int Noisy::lastID = 0;
Noisy::Noisy()
{
id = lastID++;
cerr << "Created object " << id << endl;
}
Noisy::Noisy(const Noisy& x)
{
id = lastID++;
cerr << "Copied object " << id << " from object " << x.id << endl;
}
Noisy::~Noisy()
{
cerr << "Destroyed object " << id << endl;
}
void Noisy::operator= (const Noisy&) {}
-
Keep track of when and where constructors and destructors are being invoked
-
Also helps to catch excessive copying.
Using Noisy
class TreeSet
: public Set, public Noisy
{
⋮
-
This particular mixin also works if used as a data member.
-
Should we worry about multiple inheritance conflicts?
-
Probably not, because
Noisy
declares so few public members it is unlikely to conflict with the “real” member names of my classes.
Mixin Example: checking for memory leaks
#ifndef COUNTED_H
#define COUNTED_H
class Counted
{
static int numCreated;
static int numDestroyed;
public:
Counted();
Counted(const Counted&);
virtual ~Counted();
static void report();
};
#endif
#include "counted.h"
#include <iostream>
using namespace std;
int Counted::numCreated = 0;
int Counted::numDestroyed = 0;
Counted::Counted()
{
++numCreated;
}
Counted::Counted(const Counted& x)
{
++numCreated;
}
Counted::~Counted()
{
++numDestroyed;
}
void Counted::report()
{
cerr << "Created " << numCreated << " objects." << endl;
cerr << "Destroyed " << numDestroyed << " objects." << endl;
}
-
A similar example is offered by the Counted class.
-
Lets me see if I have created more objects than I have destroyed.
3 The Observer Pattern
-
A “design pattern” or idiom of OO programming
-
Based on 2 mixins
-
Observer
-
can ask an Observable object for notification of any changes
-
Observable
-
will keep track of a list of Observers and notify them when its state changes
#ifndef OBSERVER_H
#define OBSERVER_H
//
// An Observer can register itself with any Observable object
// cell by calling the obervable's addObserver() function. Subsequently,
// the oberver wil be notified whenever the oberver calls its
// notifyObservers() function (usually whenever the obervable object's
// value has changed.
//
// Notification occurs by calling the notify() function declared here.
class Observable;
class Observer
{
public:
virtual void notify (Observable* changedObject) = 0;
};
#endif
#ifndef OBSERVABLE_H
#define OBSERVABLE_H
#include "observerptrseq.h"
// An Observable object allows any number of Observers to register
// with it. When a significant change has occured to the Observable object,
// it calls notifyObservers() and each registered observer will be notified.
// (See also oberver.h)
class Observer;
class Observable
{
public:
// Add and remove observers
void addObserver (Observer* observer);
void removeObserver (Observer* observer);
// For each registered Observer, call notify(this)
void notifyObservers();
private:
ObserverPtrSequence observers;
};
#endif
#include "observable.h"
#include "observer.h"
// An Observable object allows any number of Observers to register
// with it. When a significant change has occured to the Observable object,
// it calls notifyObservers() and each registered observer will be notified.
//
// Add and remove observers
void Observable::addObserver (Observer* observer)
{
observers.addToFront (observer);
}
void Observable::removeObserver (Observer* observer)
{
ObserverPtrSequence::Position p = observers.find(observer);
if (p != 0)
observers.remove(p);
}
// For each registered Observer, call hasChanged(this)
void Observable::notifyObservers()
{
for (ObserverPtrSequence::Position p = observers.front();
p != 0; p = observers.getNext(p))
{
observers.at(p)->notify(this);
}
}
3.1 Applications of Observer
3.1.1 Example: Propagating Changes in a Spreadsheet
Anyone who has used a spreadsheet has observed the way that, when one cell changes value, all the cells that mention that first cell in their formulas change, then all the cells the mention those cells change, and so on, in a characteristic “ripple” effect until all the effects of the original change have played out.
There are several ways to program that effect. One of the more elegant is to use the Observer/Observable pattern.
Cells Observe Each Other
class Cell: public Observable, Observer
{
public:
The idea is that cells will observe one another.
-
Suppose cell B2 contains the formula “2*A1 + B1”.
-
Then B2 will observe A1 and B1.
It will use the
Observable::addObserver
function to add itself as an observer of both those cells. -
If something were to happen to A1 that changes its value (e.g., we click on A1 and then enter a new value), then
-
A1 will call
notifyObservers()
, which goes through its list of observers, including, eventually including B2, notifying each one. -
When B2 is notified, it can re-evaluate its formula, change its value, and notify its observers.
-
Changing a Cell Formula
void Cell::putFormula(Expression* e)
{
if (theFormula != 0)
{ ➀
CellNameSequence oldReferences = theFormula->collectReferences();
for (CellNameSequence::Position p = oldReferences.front();
p != 0; p = oldReferences.getNext(p))
{
Cell* c = theSheet.getCell(oldReferences.at(p));
if (c != 0)
c->removeObserver (this);
}
delete theFormula;
}
theFormula = e; ➁
if (e != 0)
{ ➂
CellNameSequence newReferences = e->collectReferences();
for (CellNameSequence::Position p = newReferences.front();
p != 0; p = newReferences.getNext(p))
{
Cell* c = theSheet.getCell(newReferences.at(p));
if (c != 0)
c->addObserver (this);
}
}
theSheet.cellHasNewFormula (this); ➃
}
Here’s the code that’s actually invoked to change the expression stored in a cell.
-
➀ The first if statement, and it’s loop, looks at the expression already stored in the cell. It loops through all cells already named in the expression and tells them that this cell is no longer observing them (removeObserver).
-
➁ After that, we store the new expression (
e
) into the cell (theFormula
is a data member ofCell
). -
➂ The next if statement and the loop inside look almost like the first one. But now we are looking at the new formula, and calling
addObserver
instead. So now any cells mentioned in the new expression will notify this one when their values change. - ➃
we tell the spreadsheet that this cell has a new formula. The spreadsheet keeps a queue of cells that need to be re-evaluated, and processes them one at a time.
Evaluating a Cell’s Formula
const Value* Cell::evaluateFormula()
{
Value* newValue = (theFormula == 0)
? new StringValue()
: theFormula->evaluate(theSheet); ➀
if (theValue != 0 && *newValue == *theValue) ➁
delete newValue; ➂
else
{ ➃
delete theValue;
theValue = newValue;
notifyObservers(); ➄
}
return theValue;
}
Eventually the spreadsheets calls this function on our recently changed cell.
-
➀ We start by checking to see if the formula is not null. If it is not, we evaluate it to get the value of the new expression,
newValue
. -
➁ We make sure the cell’s old value (stored in the cell’s data member
theValue
) is not null, then check to see if it is equal to the new value. -
➂ If they are equal, we don’t need the new value and can throw it away.
-
➃ If they are not equal, we throw out the old value and save the new one. Now our cell’s value has definitely changed.
- ➄ So what do we do? We notify our observers.
Notifying a Cell’s Observers
void Cell::notify (Observable* changedCell)
{
theSheet.cellRequiresEvaluation (this);
}
What does an observing cell do when it is notified? It tells the spreadsheet that it needs to be re-evaluated.
- Again, the spreadsheet puts the cell into a queue, but eventually calls
evaluateFormula
on that cell,- which may change value and notify its observers,
- which will tell the spreadsheet that they need to be re-evaluated,
- which will eventually call
evaluateFormula
on them,
Eventually the propagation trickles to an end, as we eventually re-evaluate cells that either do not change value or that are not themselves mentioned in the formulae of any other cells.
3.1.2 Example: Observer and GUIs
A spreadsheet GUI contains a rectangular array of CellViews
. Each CellView
observes one Cell
-
When value in a cell changes, it notifies its observers%if _printable, as we have already seen. Some of those observers are other cells, as described earlier. But one of those observers might be a
CellView
. %endif -
The observing
CellView
then redraws itself with the new value
Scrolling the Spreadsheet
Not every cell will have a CellView
observer.
-
That’s because most of the cells in a spreadsheet are not actually visible at a given time.
-
Whenever we scroll the spreadsheet GUI, the
CellView
s each register themselves with a different cell, depending on how far we have scrolled.
Model-View-Controller (MVC) Pattern
-
A powerful approach to GUI construction
-
Separate the system into 3 subsystems
-
Model: the “real” data managed by the program
-
View: portrayal of the model
- updated as the data changes
-
Controller: input & interaction handlers
-
-
View observes the Model
MVC Interactions
How do we actually accomplish this?
- The Oberver/Observable pattern is one important component of the MVC.
- It allows the Model classes to notify appropriate parts of the GUI without knowing anything detailed about the GUI class interfaces.