Demystifying design patterns

Calum Grant

How many ways are there to write "hello world"? 
 
In this article I apply some common design patterns to the humble 'hello world' program.  The purpose of this is to illuminate and demystify design patterns using a simple example.
 
Here is the original program, hello_world.cpp.
Pattern name Source code Comments
Abstract factory abstract_factory.cpp An abstract factory is used to create objects.  However you want to change what object is created.
Adapter adapter.cpp Convert one class so that it appears to be another class.
Bridge bridge.cpp Bridge allows two implementations to vary independently. In this case, the implementations of message and recipient are independent, but combine to output "Hello world!"
Builder builder.cpp Builder is a class (or set of classes) responsible for constructing an object.  Each builder constructs a different part of the object.
Chain of responsibility chain_of_responsibility.cpp Performs staged processing using a series of different objects.
Command command.cpp Command contains an action that you need to defer for some reason. We split the task into two commands (PrintHello and PrintWorld), and queue them up in Commands.
Composite composite.cpp Composite combines several classes into a single class.
Decorator decorator.cpp Decorator adds or overrides functionality of a class.  Unlike normal inheritance, decorator overrides behaviour of a derived class, not a base class.
Façade facade.cpp Façade hides a lot of complex behaviour behind a simple, single interface.  It puts a shop-front on an otherwise complex set of classes.
Factory method factory_method.cpp Factory method creates some object (in this case a string) in a derived class, for use in a base class.
Flyweight flyweight.cpp Flyweight creates objects as they are needed, because keeping them around is unnecessary. Here the Character class is a flyweight that is only used for the duration of the inner loop.
Interpreter interpreter.cpp Interpreter processes commands and performs actions based on its input. Here we create a simple command language consisting of single characters.
Iterator iterator.cpp Iterator provides a place-holder into a collection.  In this example, StringIterator provides a placeholder into a string.  Note that C++ iterators are similar but have a different interface.
Mediator mediator.cpp Mediator binds different classes together, and the individual classes communicate via the mediator, never directly.  In this example, Message::print() calls Mediator, not Printer directly.
Memento memento.cpp Memento stores a previous state or value so that it can be restored at a later time.
Null object null_object.cpp Null object creates a special object to mean nothing/null/absent/default.  It means that the default behaviour can be implementer in the null object instead of making an explicit check for null, or using NULL pointers.
Observer observer.cpp Observer is used to watch events on another object.  Here, the Writer class keeps an eye on the MessageBoard class, and writes the message each time MessageBoard::post() is called.
Prototype prototype.cpp A Prototype is an object which is cloneable, i.e. you can create a copy, even though you don't know what you are creating a copy of.
Proxy proxy.cpp Proxy allows one object to stand for something else.  So whilst you think you are dealing with one object, you are actually dealing with something else.  In this example, CoutProxy stands for std::cout.
Singleton singleton.cpp Singleton ensures that there is one, and only one instance of a class.  Like global variables, everybody has access to that instance.  Singleton should be used sparingly since the assumption that there should be just one of something usually ends up being wrong.  Singleton is hard to write unit tests with.
State state.cpp State is like an enum, but we can define different behaviour for each state. We could add another class, Sad, which did something entirely different when asked to talk().
Strategy strategy.cpp Strategy is an algorithm that you pass into a function or class that uses the strategy.  The idea is that you can provide a different strategy to achieve something different.
Template method template_method.cpp Template method provides an implementation in a derived class, to be used by the base class.
Visitor visitor1.cpp, visitor2.cpp Visitor provides a call-back for every item in a collection.  This allows the implementation of the collection to hidden.

visitor1.cpp is the simplest type of visitor using templates.  The std::for_each template function that takes any kind of type.  visitor2.cpp uses the more traditional inheritance-based polymorphism.

Conclusion

Nowadays you cannot call yourself accomplished at object-oriented design without understanding and using design patterns.
 
There is a cautionary tale here, that design patterns can severely bloat code, and make simple tasks complicated and difficult to understand.  Abstraction has its costs.  But of course, these are just examples.
 
Design patterns are really just recipes for common design problems and solutions.  A design pattern is a solution to a problem - but if there is no problem, then don't add an unnecessary pattern!  A design should always anticipate future directions, and a well-placed pattern can make a system extensible with a minimum of effort.  A design pattern is to make the intent of a design clear, but throw too many patterns in and you get a muddle.  Programmers should generally follow the line of least resistance: don't do unnecessary work, but don't store up work for the future either.

References

  1. Gamma, Erich; Richard Helm, Ralph Johnson, and John Vlissides (1995). Design Patterns: Elements of Reusable Object-Oriented Software, hardcover, 395 pages, Addison-Wesley. ISBN 0-201-63361-2. 

Exercises

The open-closed principle states that classes should be open to extension, but closed to modification.  This means that if you want to extend or change a program, you add new classes rather than modify existing code.  Of course you may modify the main() function to pass something different to hello_world().  With this in mind, attempt the following:
  1. In adapter.cpp, write an adaptor to adapt a RoundPeg into a SquarePeg.
  2. In chain_of_responsibility.cpp, modify existing classes to make them polymorphic.  Add a class to output "Hello mom!".
  3. In the following programs, add a class to change the output to "Hello mom!": abstract_factory.cpp, bridge.cpp, builder.cpp, command.cpp, facade.cpp, factory_method.cpp, mediator.cpp, prototype.cpp, state.cpp.  Do not modify the classes that are already there!
  4. In decorator.cpp, modify the program to output "Goodbye world!".  Create a new decorator that adds a question mark.  Output "Hello world?"  Use both decorators together to output "Hello world?!"
  5. In flyweight.cpp, modify the Character class to output in upper case.
  6. In interpreter.cpp, output "Hello!"
  7. Combine the interpreter and the command patterns.
  8. In iterator.cpp, implement a method StringIterator::reset() to reset the iterator to the beginning of the string.  (Hint: You need to store the beginning of the string).
  9. In memento.cpp, implement a Memento::redo() method.
  10. In null_object.cpp, create a second null object that throws an exception.
  11. In observer.cpp, implement an observer that repeats the string twice.
  12. In singleton.cpp, change the program to do something different, without modifying existing classes (Hint: you can't).  Conclude that singleton isn't really all it's cracked up to be.
  13. In singleton.cpp, what problems might arise in a multithreaded program?  How can they be fixed?
  14. In strategy.cpp, output "Hello world..." (by adding another class)
  15. In visitor2.cpp, output "Goodbye world..."

A note on virtual functions

In C++, functions are not virtual by default.  If there are virtual functions, then it is a good idea to declare a virtual destructor, since your class is a base class and will be used polymorphically. 
 
Mixing const and virtual functions is a tricky business.  By putting a const on a virtual function, you are constraining the derived class to not modify the object.  If you really mean that, then great, however you may end up tying the hands of an implementor unnecessarily.  For a similar reason, it can be problematic for a virtual function to return a reference.

A note on references

The beauty of references is that they always point to something valid, right?  Unfortunately no.  In particular, take care when storing a reference to a temporary object.  In Java and C# you don't have that worry.