The Variant Behavior Pattern
Steven Zeil
We have seen that OOPLs offer dynamic binding as a way to determine, at run time, what function body to actually perform in response to a call.
Our code for exhibiting this has been rather contrived.
Now it’s time to look at just why dynamic binding is so important.
This design pattern, which I call1 the variant behavior pattern is at the very heart of object-oriented programming. Understand this, and you have the key to OO programming.
1 The Variant Behavior Pattern
Collections of Pointers/References to a Base Class
Suppose we have an inheritance hierarchy:
and that we have a collection of pointers or references to the BaseClass.
Apply a Virtual Function to Each Element
Then this code:
BaseClass* x;
for (each x in collection) {
x->virtualFunction(...);
}
uses dynamic binding to apply subclass-appropriate behavior to each element of a collection.
-
Each time around the loop, we extract a pointer from the collection.
Thanks to subtyping, that pointer could be pointing to something of type BaseClass or to any of its subclasses.
-
But when we call virtualFunction through that pointer, the runtime system uses the data type of the thing pointed to determine which function body to invoke.
If we have enough subclasses, we could wind up doing a different function body each time around the loop.
Study this pattern. Once you understand this, you have grasped the essence of OOP!
2 Simple Examples of the Variant Behavior pattern
There are lots of variations on this pattern. We can use almost any data structure for the collection.
2.1 Arrays of Animals
Suppose that we have an array of pointers to animals:
C++
Animal** animals = new Animal*[numberOfAnimals];
⋮
for (int i = 0; i < numberOfAnimals; ++i)
cout << animals[i]->name() << " "
<< animals[i]->eats() << endl;
Java
Animal[] animals = new Animal[numberOfAnimals];
⋮
for (Animal a: animals)
System.out.println (a.name() + " " + a.eats());
We assume that in the ⋮
area, those arrays get filled with appropriate pointer values. For example,
Animal** animals = new Animal*[numberOfAnimals];
for (int i = 0; i < numberOfAnimals; ++i)
{
char c;
cin >> c;
if (c =='A')
animals[i] = new Animal();
else if (c =='C')
animals[i] = new Carnivore();
else if (c =='H')
animals[i] = new Herbivore();
else if (c =='R')
animals[i] = new Ruminant();
}
for (int i = 0; i < numberOfAnimals; ++i)
cout << animals[i]->name() << " "
<< animals[i]->eats() << endl;
in which case, after an input of AHCR
, we would get output
animal ???
animal plants
animal meat
animal grass
although the equivalent Java code would print
animal ???
herbivore plants
carnivore meat
ruminant grass
because, in Java, both the name()
and eats()
functions are virtual.
2.2 Linked Lists of Animals
C++
struct ListNode {
Animal* data;
ListNode* next;
};
ListNode* head; // start of list
⋮
for (ListNode* current = head; current != 0; current = current->next)
cout << current->data->name() << " "
<< current->data->eats() << endl;
Java
class ListNode {
Animal data;
ListNode next;
}
ListNode head; // start of list
⋮
for (ListNode current = head; current != null;
current = current.next)
System.out.println (current.name()
+ " " + current.eats());
- Notice, once again, the complete lack of
*
or->
operators.- C++ programmers are used to seeing those as cues that they are dealing with pointers.
2.3 Vectors of Animals
C++
vector<Animal*> animals;
⋮
for (int i = 0; i < animals.size(); ++i)
cout << animals[i]->name() << " "
<< animals[i]->eats() << endl;
or
vector<Animal*> animals;
⋮
for (Animal* p: animals)
cout << p->name() << " "
<< p->eats() << endl;
Java
ArrayList<Animal> animals = new ArrayList<Animal>();
⋮
for (int i = 0; i < animals.size(); ++i) {
Animal a = animals.get(i);
System.out.println (a.name() + " " + a.eats());
}
or
ArrayList<Animal> animals = new ArrayList<Animal>();
⋮
for (Animal a: animals) {
System.out.println (a.name() + " " + a.eats());
}
2.4 Example: Trees of Animals
C++
struct TreeNode {
Animal* data;
TreeNode* leftChild;
TreeNode* rightChild;
};
TreeNode* root;
void printTree (const TreeNode* t)
{
if (t != nullptr) {
printTree(t->leftChild);
cout << t->data->name() << " "
<< t->data->eats() << endl;
printTree(t->rightChild);
}
}
⋮
printTree(root);
Java
class TreeNode {
public Animal data;
public TreeNode leftChild;
public TreeNode rightChild;
}
TreeNode root;
void printTree (TreeNode t)
{
if (t != null) {
printTree(t.leftChild);
System.out.println (
t.data.name() + " "
+ t.data.eats() );
printTree(t.rightChild);
}
}
⋮
printTree(root);
This example is a touch more subtle. There’s no loop, but the essential idea is the same. We are still iterating over a collection (in this case, using recursive calls), obtaining at each step a pointer that can point to any of several types in an inheritance hierarchy, and using that pointer to invoke a virtual function.
2.5 Summary
In all of these examples, the data structure for the collection is pretty much irrelevant. The important factors are that
-
It must be a collection of pointers/references, not direct copies of objects.
This is pretty much a given in Java, but we have to watch for this in C++.
-
The iteration must apply a virtual function to each element in the collection.
Nearly all functions are virtual in Java, but again we have to check for this in C++.
3 Larger Examples
3.1 Example: Spreadsheet – Rendering Values
Continuing our earlier example:
-
Every Cell holds a Value.
-
Every Value can be rendered into a string of a given max width. (See the render function in
value.h#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
-
Pairs of Values can be compared for equality
-
Numeric, String, and Error values are some of the possible Values
Displaying a Cell
Here is the code to draw a spreadsheet on the screen.
void NCursesSpreadSheetView::redraw() const
{
drawColumnLabels();
drawRowLabels();
CellRange shown = showing();
for (CellName cn = shown.first();
shown.more(cn); cn = shown.next(cn))
drawCell(cn);
}
After drawing the column and row labels, a call is made to showing(). That function returns a rectangular block of cell names (a CellRange) representing those cells that are currently visible on the screen, taking into account the window size, where we have scrolled to, etc.
We have a loop that goes through the collection of cell names, invoking drawCell on each one.
- This is not a virtual call. But if we look inside drawCell…
drawCell
void NCursesSpreadSheetView::drawCell
(CellName name) const
{
string cellValue;
Cell* c = sheet.getCell(name);
const Value* v = c->getValue();
if (v != 0)
{
cellValue = v->render(theColWidth);
}
centerStringInWidth (cellValue,
theColWidth);
// . . . show cellValue on screen . . .
}
Here we can see that, from the spreadsheet, we get the cell with the given name. Then from that cell we get a pointer to a value. From that pointer we call render.
render()
Now render in value.h
is virtual, and various bodies implementing it can be found in classes like NumericValue
:
std::string NumericValue::render (unsigned maxWidth) const
// 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.
{
char buffer[256];
for (char precision = '6'; precision > '0'; --precision)
{
if (maxWidth > 0)
{
sprintf (buffer, "%.1u", maxWidth);
}
else
buffer[0] = 0;
string format = string("%") + buffer + "." + precision + "g";
int width = sprintf (buffer, format.c_str(), d);
if (maxWidth == 0 || width <= maxWidth)
{
string result = buffer;
result.erase(0, result.find_first_not_of(" "));
return result;
}
}
return string(maxWidth, '*');
}
and StringValue
:
std::string StringValue::render (unsigned maxWidth) const
// 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.
{
if (maxWidth == 0 || maxWidth > s.length())
return s;
else
return s.substr(0, maxWidth);
}
and ErrorValue
:
std::string ErrorValue::render (unsigned maxWidth) const
// 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.
{
string s = theValueKindName;
if (maxWidth == 0 || maxWidth > s.length())
return s;
else
return s.substr(0, maxWidth);
}
-
So that render call will be resolved by dynamic binding, sending us to the proper function body depending on just what kind of value is actually stored in the cell.
-
Combine that with the loop in the redraw function, and we have a loop going through a collection of pointers (the value pointers inside the cells inside the spreadsheet) and using each pointer to invoke a virtual function.
3.2 Example: Evaluating a Cell
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;
}
- After evaluating a formula in a spreadsheet cell we check to see if the value obtained is equal to theValue already stored in that cell.
-
If the values are equal, we simply discard the newly computed value. We don’t need it.
-
But if they are not equal, we need to save the new value in place of the old one and trigger the re-evaluation of any cells that mention this one in their formulas.
-
(The exact mechanism for how that trigger works will be explored later.)
operator==
Look at the implementation of operator== in
#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
- It calls isEqual, which is a virtual function in
Value
.- This gives us an opportunity to select subclass-specific behavior for how to compare two values for equality, which you can see in
and
bool StringValue::isEqual (const Value& v) const
//pre: valueKind() == v.valueKind()
// Returns true iff this value is equal to v, using a comparison
// appropriate to the kind of value.
{
const StringValue& vv = dynamic_cast<const StringValue&>(v);
return s == vv.s;
}
and
bool ErrorValue::isEqual (const Value& v) const
//pre: valueKind() == v.valueKind()
// Returns true iff this value is equal to v, using a comparison
// appropriate to the kind of value.
{
return false;
}
- Can you see how the key pattern is manifested here?
- What is the container and what are the contained values?
1: Indeed, this pattern is so fundamental that most books on programming design patterns don’t bother listing it – hence my need to give it a name of my own for our discussion purposes. They take for granted that anyone reading about patterns would already know this one.