Some Common OOD Design Patterns
Steven Zeil
Abstract: In this lesson we examine some common design patterns employed by OO programmers that can contribute to the SOLID approach and to reducing coupling in general.
We have previously introduced the idea of a design pattern, an element of design that the programming community has found useful, more abstract than the specific code that implements it.
Examples of design patterns that we have already taken note of are the Iterator and Variant Behavior patterns. In this lesson, we will explore some common patterns and examine them in terms of the SOLID principles.
1 The Observer Pattern
The Observer pattern is used when we need to maintain a “consistent” view of our data across many different data objects, some of which is likely to change values frequently.
There are two classes involved:
-
the Observer
-
can ask an Observable object for notification of any changes
-
-
the 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
Here is an Observer
interface. Note that there’s not a whole lot to it. It simply defines a function by which an observer may be notify
d when an observable object changes value.
Here is the Java equivalent. The Observer/Observable pattern is so common that it is part of the standard Java API, and has been for quite some time. It has some minor differences. The function is called “update” instead of “notify” and can take a second parameter, but the basic idea is the same.
#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
Observable
is not much more complicated. It has functions allowing an observer to register itself for future notifications, and a utility function that the observable object calls to talk its current list of observers and notify each of them.
Possible implementation:
#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);
}
}
The Java version of Observable is similar.
1.1 Applications of Observer
1.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 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. -
➃ At this point, we have set up the network of cells observing other cells. This particular cell has been given a new expression, so there’s a good change its value will change once we evaluate that expression. For technical reasons, we don’t do so immediately. Instead, 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.
1.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 as we have already seen. Some of those observers are other cells, as described earlier. But one of those observers might be a
CellView
. -
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.
1.2 Observer and SOLID
-
Without the intervention of the abstract
Observer
class, all observables would need to directly make calls to their potential observers to notify them of changes.-
Compared to that possibility, the Observer pattern inverts the dependencies (DIP).
-
-
One might also make the argument that ISP is also in play, but this is mitigated by the fact that actual implementations of
notify
often wind up using downcasting on theobserver
parameter to get to the “real” subclass.
2 Model-View-Controller (MVC) Pattern
MVC is 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
-
This pattern has many advantages. GUI code is hard to test. Keeping the core data independent of the GUI means we can test it using unit or similar “easy” techniques. We can also change the GUI entirely without altering the core classes. For example, my implementation of the spreadsheet has both a graphic form (as shown in the prior section) and a text-only interface that can be used over a simple telnet connection.
Separating the control code means that we can test the view by driving it from a custom Control that simply issues a pre-scripted set of calls on the view and model classes
2.1 MVC Interactions
How do we actually accomplish this?
- The Observer/Observable pattern is often exploited in MVC.
- It allows the Model classes to notify appropriate parts of the GUI without knowing anything detailed about the GUI class interfaces.
- (ISP at work)
- It allows the Model classes to notify appropriate parts of the GUI without knowing anything detailed about the GUI class interfaces.
2.2 MVC and SOLID
- The common use of the Observer pattern to implement views promotes DIP.
- The general separation of concerns is compatible with SRP.
- The restricted directions of interactions are directly aimed at avoiding unnecessary coupling.
3 Constructing Objects of Dynamically Selected Types
There are many situations where we need to create new objects, but the exact class that we want to use is not known at compilation time.
If that problem sounds familiar, compare it to “dynamic binding”, which comes into play when we want to call a function but the exact body that we want to use is not known at compilation time.
-
But dynamic binding works by looking at the actual data type of the object being operator upon. That doesn’t help when we are trying to create a new object, because, if the object doesn’t exist yet, we can’t look to see what its type is!
-
Or, to put it another way, constructors cannot be virtual. When we invoke a constructor, we say “
new MyType(...)
” and there’s no ambiguity about what type of object we are asking for, regardless of whetherMyType
is a base class or a subclass in some inheritance hierarchy.
But, consider the problem of parsing expressions for our spreadsheet program. Let’s suppose that we have a function that has just recognized that we have an operator that is written in the form
operator ( operand )
where operand
is a subexpression that we have already parsed (for the sake of simplicity in this example, we’re only going to consider operators taking a single operand), and operator
could be sqrt
, or abs
or sin
or cos
or possibly any of a number of functions to be added later to the spreadsheet program. We really don’t want our expression parsing code to look like:
static public Expression parse(Expression e) {
Expression[] operands;
String operatorName = ...;
⋮
// We have found the operatorName and parsed a single
// operand
Expression parsedExp;
if (operatorName.equals("sqrt"))
parsedExp = new SqrtOperator(operands[0]);
else if (operatorName.equals("abs"))
parsedExp = new AbsOperator(operands[0]);
else if (operatorName.equals("sin"))
parsedExp = new SineOperator(operands[0]);
else if (operatorName.equals("cos"))
parsedExp = new CosineOperator(operands[0]);
else if ...
⋮
}
We need some way to make it easier to modify and maintain this and similar bits of code in our program that deal with creation of objects of varying types.
3.1 The Prototype Pattern
The Prototype pattern offers one approach to resolving this problem. It stems from the fact that, although we cannot construct objects by dynamic binding, we can copy objects via dynamic binding.
So, if we have one or more prototype objects available that illustrate the different classes that we want to create one of, we can copy that prototype object instead of trying to invoke its constructor directly.
For example, in our spreadsheet, we might simply store one example of each single-operand operator in an array, and search for and copy the one we want:
final static Expression[] prototypes = {
new SqrtOperator(null), new AbsOperator(null),
new SineOperator(null), new CosineOperator(null),
...
};
static public Expression parse(Expression e) {
Expression[] operands;
String operatorName = ...;
⋮
// We have found the operatorName and parsed a single
// operand
Expression parsedExp = null;
for (Expression proto: prototypes) {
if (operatorName.equals(proto.getOperatorName())) {
parsedExp = (Expression)proto.clone();
parsedExp.setOperand (0, operands[0]);
break;
}
}
⋮
}
This does not actually reduce the overall coupling of the parsing function, but it probably simplify the addition of new operator types to the program.
3.2 The Factory Pattern
I once worked on a project that processed PDF documents, occasionally needing to send selected pages to an OCR (Optical Character Recognition) program to extract the text of the page into character string form.
void processPage (PDFPage page) {
⋮
OCR ocr = OCRFactor.getOCR();
String extractedText = ocr.scanPage(page);
⋮
}
OCR programs can be expensive, and different customers of ours had already purchased different OCR programs and were not interested in buying another just to use with our software.
Our solution was to write a generic interface for doing OCR,
public interface OCR {
// public static boolean isInstalled();
public String scanPage (PDFPage page);
}
then write a class implementing that interface for each OCR program. That’s straightforward enough. But how did our program know which of those OCR classes it should use when the customer ran the code?
We added a class that checked to see whether the various OCR programs were installed on the machine, and returned an object of the appropriate class:
class OCRFactory {
if (ACMEOCR.isInstalled())
return new ACMEOCR();
else if (SoftCorpOCR.isInstalled())
return new SoftCorpOCR();
else if ...
}
This is an example of the Factory pattern.
You can see that this pattern avoids coupling the application to the various concrete subclasses. The factory itself still needs to know about the concrete instances, but if creation of those objects occurs in many places in the application, this is still an overall simplification.
There are a number of variations on this pattern. In the Abstract Factory pattern, a variety of different factories are provided n their own inheritance hierarchy, each implementing a different strategy for selecting concrete classes.
- This can be used, for example, to provide one factory for choosing among Windows options, a separate factory for choosing Linux options, etc.
A variation that I find particularly useful is to add parameters to the createinstance
function that help it to choose which instance to create. This is what I would do in our spreadsheet problem:
class ExpressionFactory {
final private static Expression[] prototypes = {
new PlusOperator(null,null), new SubtractOperator(null,null),
new TimesOperator(null,null), new DividesOperator(null,null),
new SqrtOperator(null), new AbsOperator(null),
new SineOperator(null), new CosineOperator(null),
⋮
};
public Expression createInstance (
String operatorName,
int nOperands) throws ParseError
{
for (Expression proto: prototypes) {
if (operatorName.equals(proto.getOperatorName())
&& nOperands == proto.arity()) {
return (Expression)proto.clone();
}
}
throw new ParseError();
}
}
⋮
static public Expression parse(Expression e) {
ExpressionFactory factory = new ExpressionFactory();
Expression[] operands;
String operatorName = ...;
⋮
// We have found the operatorName and parsed a single
// operand
Expression parsedExp = factory.createInstance(operatorName, 1);
⋮
}
This moves all of the Expression
-subclass coupling out of the Expression
class, which
- is complicated enough already, and
- would be problematic since,
- if
Expression
depends on its own subclasses - and, by definition, every subclass depends on its base class
- if