A design pattern is a standard approach to program organization that solves a general problem. C++ is an object-oriented language, so the design patterns of interest to C++ programmers are generally object-oriented patterns, which describe strategies for organizing objects and object relationships in your programs. These patterns are usually applicable to any object-oriented language, such as C++, C#, Java, or Smalltalk. In fact, if you are familiar with C# or Java programming, you will recognize many of these patterns.
Design patterns are less language-specific than are techniques. The difference between a pattern and a technique is admittedly fuzzy, and different books employ different definitions. This book defines a technique as a strategy particular to the C++ language, while a pattern is a more general strategy for object-oriented design applicable to any object-oriented language.
Note that many patterns have several different names. The distinctions between the patterns themselves can be somewhat vague, with different sources describing and categorizing them slightly differently. In fact, depending on the books or other sources you use, you may find the same name applied to different patterns. There is even disagreement as to which design approaches qualify as patterns. With a few exceptions, this book follows the terminology used in the seminal book Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma et al. (Addison-wesley Professional, 1994). However, other pattern names and variations are noted when appropriate.
The design pattern concept is a simple but powerful idea. Once you are able to recognize the recurring object-oriented interactions that occur in a program, finding an elegant solution becomes a matter of merely selecting the appropriate pattern to apply. This chapter describes several design patterns in detail and presents sample implementations.
Certain patterns go by different names or are subject to different interpretations. Any aspect of design is likely to provoke debate among programmers, and I believe that is a good thing. Don’t simply accept these patterns as the only way to accomplish a task—draw on their approaches and ideas to refine them and form new patterns.
The iterator pattern provides a mechanism for separating algorithms or operations from the data on which they operate. At first glance, this pattern seems to contradict the fundamental principle in object-oriented programming of grouping together in objects data and the behaviors that operate on that data. While that argument is true on a certain level, the iterator pattern does not advocate removing fundamental behaviors from objects. Instead, it solves two problems that commonly arise with tight coupling of data and behaviors.
The first problem with tightly coupling data and behaviors is that it precludes generic algorithms that work on a variety of objects, not all of which are in the same class hierarchy. In order to write generic algorithms, you need some standard mechanism to access the contents of the objects.
The second problem with tightly coupled data and behaviors is that it’s sometimes difficult to add new behaviors. At the very least, you need access to the source code for the data objects. However, what if the object hierarchy of interest is part of a third-party framework or library that you cannot change? It would be nice to be able to add an algorithm or operation that works on the data without modifying the original object hierarchy of classes that hold the data.
You’ve already seen an example of the iterator pattern in the Standard Library. Conceptually, iterators provide a mechanism for an operation or algorithm to access a container of elements in a sequence. The name comes from the English word iterate, which means “repeat.” It applies to iterators because they repeat the action of moving forward in the sequence to reach each new element. In the Standard Library, the generic algorithms use iterators to access the elements of the containers on which they operate. By defining a standard iterator interface, the Standard Library allows you to write algorithms that can work on any container that supplies an iterator with the appropriate interface. Thus, iterators allow you to write generic algorithms without modifying the classes that hold the data. Figure 29-1 shows an iterator as the central coordinator; operations depend on iterators, and data objects provide iterators.
Chapter 21 illustrates a detailed example of how to implement an iterator for a class that conforms to the Standard Library requirements, which means that its iterator can be used by the generic Standard Library algorithms.
The singleton is one of the simplest design patterns. In English, the word singleton means “one of a kind” or “individual.” It has a similar meaning in programming. The singleton pattern is a strategy for enforcing the existence of exactly one instance of a class in a program. Applying the singleton pattern to a class guarantees that only one object of that class will ever be created. The singleton pattern also specifies that that one object is globally accessible from anywhere in the program. Programmers usually refer to a class following the singleton pattern as a singleton class.
If your program relies on the assumption that there will be exactly one instance of a class, you could enforce that assumption with the singleton pattern.
However, the singleton pattern has a number of disadvantages that you need to be aware of. If you have multiple singletons, it’s not always easy to guarantee that they are initialized in the right order at program startup. It’s also not easy to ensure a singleton is still there when callers need it during program shutdown. On top of that, singleton classes introduce hidden dependencies, cause tight coupling, and complicate unit testing. In a unit test, for example, you might want to write a stub version (see Chapter 26) of a singleton, but given the nature of a typical singleton implementation, that’s hard to do. A more appropriate design pattern could be dependency injection. With dependency injection, you create an interface for each service you provide, and inject the interfaces a component needs into the component. Dependency injection allows mocking (stub versions), makes it easier to introduce multiple instances later on, allows for more complicated ways of constructing the single object than a typical singleton, for example using factories, and so on. Still, the singleton pattern is discussed here because you will encounter it.
Singletons can be useful for utility classes. Many applications have a notion of a logger—a class that is responsible for writing status information, debugging data, and errors to a central location. The ideal logging class has the following characteristics:
The singleton pattern is a good match for a logger because, even though the logger could be used in many different contexts and for many different purposes, it is conceptually a single instance. Implementing the logger class as a singleton also makes it easier to use because you never have to worry about which logger is the current one or how to get a hold of the current logger; there’s only one, so it’s a moot point!
There are two basic ways to implement singleton behavior in C++. The first approach uses a class with only static methods. Such a class needs no instantiation and is accessible from anywhere. The problem with this method is that it lacks a built-in mechanism for construction and destruction. However, technically, a class that uses all static methods isn’t really a singleton: it’s a nothington or a static class, to coin new terms. The term singleton implies that there is exactly one instance of the class. If all of the methods are static and the class is never instantiated at all, you cannot really call it a singleton anymore. So, this is not further discussed in this section.
The second approach uses access control levels to regulate the creation and access of one single instance of a class. This is a true singleton and discussed further with an example of a simple Logger
class, which provides the following features:
To build a true singleton in C++, you can use the access control mechanisms as well as the static
keyword. An actual Logger
object exists at run time, and the class enforces that only one object is ever instantiated. Clients can always get a hold of that object through a static method called instance()
. The class definition looks like this:
// Definition of a singleton logger class.
class Logger final
{
public:
enum class LogLevel {
Error,
Info,
Debug
};
// Returns a reference to the singleton Logger object.
static Logger& instance();
// Prevent copy/move construction.
Logger(const Logger&) = delete;
Logger(Logger&&) = delete;
// Prevent copy/move assignment operations.
Logger& operator=(const Logger&) = delete;
Logger& operator=(Logger&&) = delete;
// Sets the log level.
void setLogLevel(LogLevel level);
// Logs a single message at the given log level.
void log(std::string_view message, LogLevel logLevel);
// Logs a vector of messages at the given log level.
void log(const std::vector<std::string>& messages,
LogLevel logLevel);
private:
// Private constructor and destructor.
Logger();
~Logger();
// Converts a log level to a human readable string.
std::string_view getLogLevelString(LogLevel level) const;
static const char* const kLogFileName;
std::ofstream mOutputStream;
LogLevel mLogLevel = LogLevel::Error;
};
This implementation is based on Scott Meyer’s singleton pattern. This means that the instance()
method contains a local static
instance of the Logger
class. C++ guarantees that this local static
instance is initialized in a thread-safe fashion, so you don’t need any manual thread synchronization in this version of the singleton pattern. These are so-called magic statics. Note that only the initialization is thread safe! If multiple threads are going to call methods on the Logger
class, then you should make the Logger
methods themselves thread safe as well. See Chapter 23 for a detailed discussion on synchronization mechanisms to make a class thread safe.
The implementation of the Logger
class is fairly straightforward. Once the log file has been opened, each log message is written to it with the log level prepended. The constructor and destructor are called automatically when the static
instance of the Logger
class in the instance()
method is created and destroyed. Because the constructor and destructor are private, no external code can create or delete a Logger
. Here is the implementation:
#include "Logger.h"
#include <stdexcept>
using namespace std;
const char* const Logger::kLogFileName = "log.out"; //*
Logger& Logger::instance()
{
static Logger instance;
return instance;
}
Logger::Logger()
{
mOutputStream.open(kLogFileName, ios_base::app);
if (!mOutputStream.good()) {
throw runtime_error("Unable to initialize the Logger!");
}
}
Logger::~Logger()
{
mOutputStream << "Logger shutting down." << endl;
mOutputStream.close();
}
void Logger::setLogLevel(LogLevel level)
{
mLogLevel = level;
}
string_view Logger::getLogLevelString(LogLevel level) const
{
switch (level) {
case LogLevel::Error:
return "ERROR";
case LogLevel::Info:
return "INFO";
case LogLevel::Debug:
return "DEBUG";
}
throw runtime_error("Invalid log level.");
}
void Logger::log(string_view message, LogLevel logLevel)
{
if (mLogLevel < logLevel) {
return;
}
mOutputStream << getLogLevelString(logLevel).data()
<< ": " << message << endl;
}
void Logger::log(const vector<string>& messages, LogLevel logLevel)
{
if (mLogLevel < logLevel) {
return;
}
for (const auto& message : messages) {
log(message, logLevel);
}
}
If your compiler supports C++17 inline variables, introduced in Chapter 9, then you can remove the line marked with //*
from the source file, and instead define it as an inline variable directly in the class definition, as follows:
class Logger final
{
// Omitted for brevity
static inline const char* const kLogFileName = "log.out";
};
The singleton Logger
class can be tested as follows:
// Set log level to Debug.
Logger::instance().setLogLevel(Logger::LogLevel::Debug);
// Log some messages.
Logger::instance().log("test message", Logger::LogLevel::Debug);
vector<string> items = {"item1", "item2"};
Logger::instance().log(items, Logger::LogLevel::Error);
// Set log level to Error.
Logger::instance().setLogLevel(Logger::LogLevel::Error);
// Now that the log level is set to Error, logging a Debug
// message will be ignored.
Logger::instance().log("A debug message", Logger::LogLevel::Debug);
After executing, the file log.out
contains the following lines:
DEBUG: test message
ERROR: item1
ERROR: item2
Logger shutting down.
A factory in real life constructs tangible objects, such as tables or cars. Similarly, a factory in object-oriented programming constructs objects. When you use factories in your program, portions of code that want to create a particular object ask the factory for an instance of the object instead of calling the object constructor themselves. For example, an interior decorating program might have a FurnitureFactory
object. When part of the code needs a piece of furniture such as a table, it would call the createTable()
method of the FurnitureFactory
object, which would return a new table.
At first glance, factories seem to lead to complicated designs without clear benefits. It appears that you’re only adding another layer of complexity to the program. Instead of calling createTable()
on a FurnitureFactory
, you could simply create a new Table
object directly. However, factories can actually be quite useful. Instead of creating various objects all over the program, you centralize the object creation for a particular domain.
Another benefit of factories is that you can use them alongside class hierarchies to construct objects without knowing their exact class. As you’ll see in the following example, factories can run parallel to class hierarchies. This is not to say they must run parallel to class hierarchies. Factories may as well just create any number of concrete types.
Another reason to use a factory is that maybe the creation of your objects requires certain information, state, resources, and so on, owned by the factory. A factory can also be used if creating your objects requires a complex series of steps to be executed in the right order, or if all created objects need to be linked to other objects in a correct manner, and so on.
One of the main benefits is that factories abstract the object creation process; using dependency injection, you can easily substitute a different factory in your program. Just as you can use polymorphism with the created objects, you can use polymorphism with factories. The following example demonstrates this.
In the real world, when you talk about driving a car, you can do so without referring to the specific type of car. You could be discussing a Toyota or a Ford. It doesn’t matter, because both Toyotas and Fords are drivable. Now, suppose that you want a new car. You would then need to specify whether you wanted a Toyota or a Ford, right? Not always. You could just say, “I want a car,” and depending on where you were, you would get a specific car. If you said, “I want a car,” in a Toyota factory, chances are you’d get a Toyota. (Or you’d get arrested, depending on how you asked.) If you said, “I want a car,” in a Ford factory, you’d get a Ford.
The same concepts apply to C++ programming. The first concept, a generic car that’s drivable, is nothing new; it’s standard polymorphism, described in Chapter 5. You could write an abstract Car
class that defines a virtual drive()
method. Both Toyota
and Ford
could be derived classes of the Car
class, as shown in Figure 29-2.
Your program could drive Car
s without knowing whether they were really Toyota
s or Ford
s. However, with standard object-oriented programming, the one place that you’d need to specify Toyota
or Ford
would be when you created the car. Here, you would need to call the constructor for one or the other. You couldn’t just say, “I want a car.” However, suppose that you also had a parallel class hierarchy of car factories. The CarFactory
base class could define a virtual requestCar()
method. The ToyotaFactory
and FordFactory
derived classes would override the requestCar()
method to build a Toyota
or a Ford
. Figure 29-3 shows the CarFactory
hierarchy.
Now, suppose that there is one CarFactory
object in a program. When code in the program, such as a car dealer, wants a new car, it calls requestCar()
on the CarFactory
object. Depending on whether that car factory is really a ToyotaFactory
or a FordFactory
, the code gets either a Toyota
or a Ford
. Figure 29-4 shows the objects in a car dealer program using a ToyotaFactory
.
Figure 29-5 shows the same program, but with a FordFactory
instead of a ToyotaFactory
. Note that the CarDealer
object and its relationship with the factory stay the same.
This example demonstrates using polymorphism with factories. When you ask the car factory for a car, you might not know whether it’s a Toyota factory or a Ford factory, but either way it will give you a Car
that you can drive. This approach leads to easily extensible programs; simply changing the factory instance can allow the program to work on a completely different set of objects and classes.
One reason for using factories is that the type of the object you want to create may depend on some condition. For example, if you want a car, you might want to put your order into the factory that has received the fewest requests so far, regardless of whether the car you eventually get is a Toyota
or a Ford
. The following implementation shows how to write such factories in C++.
The first thing you’ll need is the hierarchy of cars. To keep this example concise, the Car
class simply has an abstract method that returns a description of the car:
class Car
{
public:
virtual ~Car() = default; // Always a virtual destructor!
virtual std::string_view info() const = 0;
};
class Ford : public Car
{
public:
virtual std::string_view info() const override { return "Ford"; }
};
class Toyota : public Car
{
public:
virtual std::string_view info() const override { return "Toyota"; }
};
The CarFactory
base class is a bit more interesting. Each factory keeps track of the number of cars produced. When the public requestCar()
method is called, the number of cars produced at the factory is increased by one, and the pure virtual createCar()
method is called, which creates and returns a new car. The idea is that individual factories override createCar()
to return the appropriate type of car. The CarFactory
itself implements requestCar()
, which takes care of updating the number of cars produced. The CarFactory
also provides a public
method to query the number of cars produced at each factory.
The class definitions for the CarFactory
class and derived classes are as follows:
#include "Car.h"
#include <cstddef>
#include <memory>
class CarFactory
{
public:
virtual ~CarFactory() = default; // Always a virtual destructor!
std::unique_ptr<Car> requestCar();
size_t getNumberOfCarsProduced() const;
protected:
virtual std::unique_ptr<Car> createCar() = 0;
private:
size_t mNumberOfCarsProduced = 0;
};
class FordFactory : public CarFactory
{
protected:
virtual std::unique_ptr<Car> createCar() override;
};
class ToyotaFactory : public CarFactory
{
protected:
virtual std::unique_ptr<Car> createCar() override;
};
As you can see, the derived classes simply override createCar()
to return the specific type of car that they produce. The implementation of the CarFactory
hierarchy is as follows:
// Increment the number of cars produced and return the new car.
std::unique_ptr<Car> CarFactory::requestCar()
{
++mNumberOfCarsProduced;
return createCar();
}
size_t CarFactory::getNumberOfCarsProduced() const
{
return mNumberOfCarsProduced;
}
std::unique_ptr<Car> FordFactory::createCar()
{
return std::make_unique<Ford>();
}
std::unique_ptr<Car> ToyotaFactory::createCar()
{
return std::make_unique<Toyota>();
}
The implementation approach used in this example is called an abstract factory because the type of object created depends on which concrete derived class of the factory class is being used. A similar pattern can be implemented in a single class instead of a class hierarchy. In that case, a single create()
method takes a type or string parameter from which it decides which object to create. For example, a CarFactory
class could provide a requestCar()
method that takes a string representing the type of car to build, and constructs the appropriate car.
The simplest way to use a factory is to instantiate it and to call the appropriate method, as in the following piece of code:
ToyotaFactory myFactory;
auto myCar = myFactory.requestCar();
cout << myCar->info() << endl; // Outputs Toyota
A more interesting example makes use of the virtual constructor idea to build a car in the factory that has the fewest cars produced. To do this, you can create a new factory, called LeastBusyFactory
, that derives from CarFactory
and that accepts a number of other CarFactory
objects in its constructor. As all CarFactory
classes have to do, LeastBusyFactory
overrides the createCar()
method. Its implementation finds the least busy factory in the list of factories passed to the constructor, and asks that factory to create a car. Here is the implementation of such a factory:
class LeastBusyFactory : public CarFactory
{
public:
// Constructs a LeastBusyFactory instance, taking ownership of
// the given factories.
explicit LeastBusyFactory(vector<unique_ptr<CarFactory>>&& factories);
protected:
virtual unique_ptr<Car> createCar() override;
private:
vector<unique_ptr<CarFactory>> mFactories;
};
LeastBusyFactory::LeastBusyFactory(vector<unique_ptr<CarFactory>>&& factories)
: mFactories(std::move(factories))
{
if (mFactories.empty())
throw runtime_error("No factories provided.");
}
unique_ptr<Car> LeastBusyFactory::createCar()
{
CarFactory* bestSoFar = mFactories[0].get();
for (auto& factory : mFactories) {
if (factory->getNumberOfCarsProduced() <
bestSoFar->getNumberOfCarsProduced()) {
bestSoFar = factory.get();
}
}
return bestSoFar->requestCar();
}
The following code makes use of this factory to build ten cars, whatever brand they might be, from the factory that has produced the least number of cars.
vector<unique_ptr<CarFactory>> factories;
// Create 3 Ford factories and 1 Toyota factory.
factories.push_back(make_unique<FordFactory>());
factories.push_back(make_unique<FordFactory>());
factories.push_back(make_unique<FordFactory>());
factories.push_back(make_unique<ToyotaFactory>());
// To get more interesting results, preorder some cars.
factories[0]->requestCar();
factories[0]->requestCar();
factories[1]->requestCar();
factories[3]->requestCar();
// Create a factory that automatically selects the least busy
// factory from a list of given factories.
LeastBusyFactory leastBusyFactory(std::move(factories));
// Build 10 cars from the least busy factory.
for (size_t i = 0; i < 10; i++) {
auto theCar = leastBusyFactory.requestCar();
cout << theCar->info() << endl;
}
When executed, the program prints out the make of each car produced:
Ford
Ford
Ford
Toyota
Ford
Ford
Ford
Toyota
Ford
Ford
The results are rather predictable because the loop effectively iterates through the factories in a round-robin fashion. However, one could imagine a scenario where multiple dealers are requesting cars, and the current status of each factory isn’t quite so predictable.
You can also use the factory pattern for more than just modeling real-world factories. For example, consider a word processor in which you want to support documents in different languages, where each document uses a single language. There are many aspects of the word processor in which the choice of document language requires different support: the character set used in the document (whether or not accented characters are needed), the spell checker, the thesaurus, and the way the document is displayed, to name just a few. You could use factories to design a clean word processor by writing an abstract LanguageFactory
base class and concrete factories for each language of interest, such as EnglishLanguageFactory
and FrenchLanguageFactory
. When the user specifies a language for a document, the program instantiates the appropriate language factory and attaches it to the document. From then on, the program doesn’t need to know which language is supported in the document. When it needs a language-specific piece of functionality, it can just ask the LanguageFactory
. For example, when it needs a spell checker, it can call the createSpellchecker()
method on the factory, which will return a spell checker in the appropriate language.
The proxy pattern is one of several patterns that divorce the abstraction of a class from its underlying representation. A proxy object serves as a stand-in for a real object. Such objects are generally used when using the real object would be time-consuming or impossible. For example, take a document editor. A document could contain several big objects, such as images. Instead of loading all those images when opening the document, the document editor could substitute all images with image proxies. These proxies don’t immediately load the images. Only when the user scrolls down in the document and reaches an image, does the document editor ask the image proxy to draw itself. At that time, the proxy delegates the work to the real image class, which loads the image.
Consider a networked game with a Player
class that represents a person on the Internet who has joined the game. The Player
class includes functionality that requires network connectivity, such as an instant messaging feature. If a player’s connection becomes slow or unresponsive, the Player
object representing that person can no longer receive instant messages.
Because you don’t want to expose network problems to the user, it may be desirable to have a separate class that hides the networked parts of a Player
. This PlayerProxy
object would substitute for the actual Player
object. Either clients of the class would use the PlayerProxy
class at all times as a gatekeeper to the real Player
class, or the system would substitute a PlayerProxy
when a Player
became unavailable. During a network failure, the PlayerProxy
object could still display the player’s name and last-known state, and could continue to function when the original Player
object could not. Thus, the proxy class hides some undesirable semantics of the underlying Player
class.
The first step is defining an IPlayer
interface containing the public interface for a Player
.
class IPlayer
{
public:
virtual std::string getName() const = 0;
// Sends an instant message to the player over the network and
// returns the reply as a string.
virtual std::string sendInstantMessage(
std::string_view message) const = 0;
};
The Player
class definition then becomes as follows. The sendInstantMessage()
method of a Player
requires network connectivity to properly function.
class Player : public IPlayer
{
public:
virtual std::string getName() const override;
// Network connectivity is required.
virtual std::string sendInstantMessage(
std::string_view message) const override;
};
The PlayerProxy
class also derives from IPlayer
, and contains another IPlayer
instance (the ‘real’ Player
):
class PlayerProxy : public IPlayer
{
public:
// Create a PlayerProxy, taking ownership of the given player.
PlayerProxy(std::unique_ptr<IPlayer> player);
virtual std::string getName() const override;
// Network connectivity is optional.
virtual std::string sendInstantMessage(
std::string_view message) const override;
private:
std::unique_ptr<IPlayer> mPlayer;
};
The constructor takes ownership of the given IPlayer
:
PlayerProxy::PlayerProxy(std::unique_ptr<IPlayer> player)
: mPlayer(std::move(player))
{
}
The implementation of the PlayerProxy
’s sendInstantMessage()
method checks the network connectivity, and either returns a default string or forwards the request.
std::string PlayerProxy::sendInstantMessage(std::string_view message) const
{
if (hasNetworkConnectivity())
return mPlayer->sendInstantMessage(message);
else
return "The player has gone offline.";
}
If a proxy is well written, using it should be no different from using any other object. For the PlayerProxy
example, the code that uses the proxy could be completely unaware of its existence. The following function, designed to be called when the Player
has won, could be dealing with an actual Player
or a PlayerProxy
. The code is able to handle both cases in the same way because the proxy ensures a valid result.
bool informWinner(const IPlayer& player)
{
auto result = player.sendInstantMessage("You have won! Play again?");
if (result == "yes") {
cout << player.getName() << " wants to play again." << endl;
return true;
} else {
// The player said no, or is offline.
cout << player.getName() << " does not want to play again." << endl;
return false;
}
}
The motivation for changing the abstraction given by a class is not always driven by a desire to hide functionality. Sometimes, the underlying abstraction cannot be changed but it doesn’t suit the current design. In this case, you can build an adaptor or wrapper class. The adaptor provides the abstraction that the rest of the code uses and serves as the bridge between the desired abstraction and the actual underlying code. Chapter 17 discusses how the Standard Library uses the adaptor pattern to implement containers like stack
and queue
in terms of other containers, such as deque
and list
.
For this adaptor pattern example, let’s assume a very basic Logger
class. Here is the class definition:
class Logger
{
public:
enum class LogLevel {
Error,
Info,
Debug
};
Logger();
virtual ~Logger() = default; // Always a virtual destructor!
void log(LogLevel level, std::string message);
private:
// Converts a log level to a human readable string.
std::string_view getLogLevelString(LogLevel level) const;
};
And here are the implementations:
Logger::Logger()
{
cout << "Logger constructor" << endl;
}
void Logger::log(LogLevel level, std::string message)
{
cout << getLogLevelString(level).data() << ": " << message << endl;
}
string_view Logger::getLogLevelString(LogLevel level) const
{
// Same implementation as the Singleton logger earlier in this chapter.
}
The Logger
class has a constructor, which outputs a line of text to the standard console, and a method called log()
that writes the given message to the console prefixed with a log level.
One reason why you might want to write a wrapper class around this basic Logger
class is to change its interface. Maybe you are not interested in the log level and you would like to call the log()
method with only one parameter, the actual message. You might also want to change the interface to accept an std::string_view
instead of an std::string
as parameter for the log()
method.
The first step in implementing the adaptor pattern is to define the new interface for the underlying functionality. This new interface is called NewLoggerInterface
and looks like this:
class NewLoggerInterface
{
public:
virtual ~NewLoggerInterface() = default; // Always virtual destructor!
virtual void log(std::string_view message) = 0;
};
This class is an abstract class, which declares the desired interface that you want for your new logger. The interface only defines one abstract method, that is, a log()
method accepting only a single argument of type string_view
, which needs to be implemented by any class implementing this interface.
The next step is to write the actual new logger class, NewLoggerAdaptor
, which implements NewLoggerInterface
so that it has the interface that you designed. The implementation wraps a Logger
instance; it uses composition.
class NewLoggerAdaptor : public NewLoggerInterface
{
public:
NewLoggerAdaptor();
virtual void log(std::string_view message) override;
private:
Logger mLogger;
};
The constructor of the new class writes a line to standard output to keep track of which constructors are being called. The code then implements the log()
method from NewLoggerInterface
by forwarding the call to the log()
method of the Logger
instance that is wrapped. In that call, the given string_view
is converted to a string
, and the log level is hard-coded as Info
:
NewLoggerAdaptor::NewLoggerAdaptor()
{
cout << "NewLoggerAdaptor constructor" << endl;
}
void NewLoggerAdaptor::log(string_view message)
{
mLogger.log(Logger::LogLevel::Info, message.data());
}
Because adaptors exist to provide a more appropriate interface for the underlying functionality, their use should be straightforward and specific to the particular case. Given the previous implementation, the following code snippet uses the new simplified interface for the Logger
class:
NewLoggerAdaptor logger;
logger.log("Testing the logger.");
It produces the following output:
Logger constructor
NewLoggerAdaptor constructor
INFO: Testing the logger.
The decorator pattern is exactly what it sounds like: a “decoration” on an object. It is also often called a wrapper. The pattern is used to add or change the behavior of an object at run time. Decorators are a lot like derived classes, but their effects can be temporary. For example, if you have a stream of data that you are parsing and you reach data that represents an image, you could temporarily decorate the stream object with an ImageStream
object. The ImageStream
constructor would take the stream object as a parameter and would have built-in knowledge of image parsing. Once the image is parsed, you could continue using the original object to parse the remainder of the stream. The ImageStream
acts as a decorator because it adds new functionality (image parsing) to an existing object (a stream).
As you may already know, web pages are written in a simple text-based structure called HyperText Markup Language (HTML). In HTML, you can apply styles to a text by using style tags, such as <B> and </B> for bold and <I> and </I> for italic. The following line of HTML displays the message in bold:
<B>A party? For me? Thanks!</B>
The following line displays the message in bold and italic:
<I><B>A party? For me? Thanks!</B></I>
Suppose you are writing an HTML editing application. Your users will be able to type in paragraphs of text and apply one or more styles to them. You could make each type of paragraph a new derived class, as shown in Figure 29-6, but that design could be cumbersome and would grow exponentially as new styles were added.
The alternative is to consider styled paragraphs not as types of paragraphs, but as decorated paragraphs. This leads to situations like the one shown in Figure 29-7, where an ItalicParagraph
operates on a BoldParagraph
, which in turn operates on a Paragraph
. The recursive decoration of objects nests the styles in code just as they are nested in HTML.
To start, you need an IParagraph
interface:
class IParagraph
{
public:
virtual ~IParagraph() = default; // Always a virtual destructor!
virtual std::string getHTML() const = 0;
};
The Paragraph
class implements this IParagraph
interface:
class Paragraph : public IParagraph
{
public:
Paragraph(std::string_view text) : mText(text) {}
virtual std::string getHTML() const override { return mText; }
private:
std::string mText;
};
To decorate a Paragraph
with zero or more styles, you need styled IParagraph
classes, each one constructible from an existing IParagraph
. This way, they can all decorate a Paragraph
or a styled IParagraph
. The BoldParagraph
class derives from IParagraph
and implements getHTML()
. However, because you only intend to use it as a decorator, its single public non-copy constructor takes a const
reference to an IParagraph
.
class BoldParagraph : public IParagraph
{
public:
BoldParagraph(const IParagraph& paragraph) : mWrapped(paragraph) {}
virtual std::string getHTML() const override {
return "<B>" + mWrapped.getHTML() + "</B>";
}
private:
const IParagraph& mWrapped;
};
The ItalicParagraph
class is almost identical:
class ItalicParagraph : public IParagraph
{
public:
ItalicParagraph(const IParagraph& paragraph) : mWrapped(paragraph) {}
virtual std::string getHTML() const override {
return "<I>" + mWrapped.getHTML() + "</I>";
}
private:
const IParagraph& mWrapped;
};
From the user’s point of view, the decorator pattern is appealing because it is very easy to apply, and is transparent once applied. The user doesn’t need to know that a decorator has been employed at all. A BoldParagraph
behaves just like a Paragraph
.
Here is a quick example that creates and outputs a paragraph, first in bold, then in bold and italic:
Paragraph p("A party? For me? Thanks!");
// Bold
std::cout << BoldParagraph(p).getHTML() << std::endl;
// Bold and Italic
std::cout << ItalicParagraph(BoldParagraph(p)).getHTML() << std::endl;
The output is as follows:
<B>A party? For me? Thanks!</B>
<I><B>A party? For me? Thanks!</B></I>
A chain of responsibility is used when you want a number of objects to get a crack at performing a particular action. The technique can employ polymorphism so that the most specific class gets called first and can either handle the call or pass it up to its parent. The parent then makes the same decision—it can handle the call or pass it up to its parent. A chain of responsibility does not necessarily have to follow a class hierarchy, but it typically does.
Chains of responsibility are perhaps most commonly used for event handling. Many modern applications, particularly those with graphical user interfaces, are designed as a series of events and responses. For example, when a user clicks on the File menu and selects Open, an open event has occurred. When the user moves the mouse over the drawable area of a paint program, mouse move events are generated continuously. If the user presses down a button on the mouse, a mouse down event for that button-press is generated. The program can then start paying attention to the mouse move events, allowing the user to “draw” some object, and continue doing this until the mouse up event occurs. Each operating system has its own way of naming and using these events, but the overall idea is the same: when an event occurs, it is somehow communicated to the program, which takes appropriate action.
As you know, C++ does not have any built-in facilities for graphical programming. It also has no notion of events, event transmission, or event handling. A chain of responsibility is a reasonable approach to event handling to give different objects a chance to handle certain events.
Consider a drawing program, which has a hierarchy of Shape
classes, as in Figure 29-8.
The leaf nodes can handle certain events. For example, Circle
can receive mouse move events to change the radius of the circle. The parent class handles events that have the same effect, regardless of the particular shape. For example, a delete event is handled the same way, regardless of the type of shape being deleted. With a chain of responsibility, the leaf nodes get the first crack at handling a particular event. If that leaf node cannot handle the event, it explicitly forwards the event to the next handler in the chain, and so on. For example, if a mouse down event occurs on a Square
object, first the Square
gets a chance to handle the event. If it doesn’t handle the event, it forwards the event to the next handler in the chain, which is the Shape
class for this example. Now the Shape
class gets a crack at handling the event. If Shape
can’t handle the event either, it could forward it to its parent if it had one, and so on. This continues all the way up the chain. It’s a chain of responsibility because each handler may either handle the event, or pass the event up to the next handler in the chain.
The code for a chained messaging approach varies based on how your operating system handles events, but it tends to resemble the following code, which uses integers to represent types of events:
void Square::handleMessage(int message)
{
switch (message) {
case kMessageMouseDown:
handleMouseDown();
break;
case kMessageInvert:
handleInvert();
break;
default:
// Message not recognized--chain to base class.
Shape::handleMessage(message);
}
}
void Shape::handleMessage(int message)
{
switch (message) {
case kMessageDelete:
handleDelete();
break;
default:
{
stringstream ss;
ss << __func__ << ": Unrecognized message received: " << message;
throw invalid_argument(ss.str());
}
}
}
When the event-handling portion of the program or framework receives a message, it finds the corresponding shape and calls handleMessage()
. Through polymorphism, the derived class’s version of handleMessage()
is called. This gives the leaf node the first chance at handling the message. If it doesn’t know how to handle the message, it passes it up to its base class, which gets the next chance. In this example, the final recipient of the message throws an exception if it is unable to handle the event. You could also have your handleMessage()
method return a Boolean indicating success or failure.
This chain of responsibility example can be tested as follows. For the chain to respond to events, there must be another class that dispatches the events to the correct object. Because this task varies greatly by framework or platform, the following example shows pseudo-code for handling a mouse down event, in lieu of platform-specific C++ code:
MouseLocation loc = getClickLocation();
Shape* clickedShape = findShapeAtLocation(loc);
if (clickedShape)
clickedShape->handleMessage(kMessageMouseDown);
The chained approach is flexible and has a very appealing structure for object-oriented hierarchies. The downside is that it requires diligence on the part of the programmer. If you forget to chain up to the base class from a derived class, events will effectively get lost. Worse, if you chain to the wrong class, you could end up in an infinite loop! Note that while event chains usually correlate with a class hierarchy, they do not have to. In the preceding example, the Square
class could have just as easily passed the message to an entirely different object. An example of such a chain of responsibility is given in the next section.
If the different handlers in a chain of responsibility are not related by a class hierarchy, you need to keep track of the chain yourself. Here is an example. First, a Handler
mixin class is defined:
class Handler
{
public:
virtual ~Handler() = default;
Handler(Handler* nextHandler) : mNextHandler(nextHandler) { }
virtual void handleMessage(int message)
{
if (mNextHandler)
mNextHandler->handleMessage(message);
}
private:
Handler* mNextHandler;
};
Next, two concrete handlers are defined, both deriving from the Handler
mixin. The first handles only messages with ID 1, and the second handles only messages with ID 2. If any of the handlers receives a message it doesn’t know about, it calls the next handler in the chain.
class ConcreteHandler1 : public Handler
{
public:
ConcreteHandler1(Handler* nextHandler) : Handler(nextHandler) {}
void handleMessage(int message) override
{
cout << "ConcreteHandler1::handleMessage()" << endl;
if (message == 1)
cout << "Handling message " << message << endl;
else {
cout << "Not handling message " << message << endl;
Handler::handleMessage(message);
}
}
};
class ConcreteHandler2 : public Handler
{
public:
ConcreteHandler2(Handler* nextHandler) : Handler(nextHandler) {}
void handleMessage(int message) override
{
cout << "ConcreteHandler2::handleMessage()" << endl;
if (message == 2)
cout << "Handling message " << message << endl;
else {
cout << "Not handling message " << message << endl;
Handler::handleMessage(message);
}
}
};
This implementation can be tested as follows:
ConcreteHandler2 handler2(nullptr);
ConcreteHandler1 handler1(&handler2);
handler1.handleMessage(1);
cout << endl;
handler1.handleMessage(2);
cout << endl;
handler1.handleMessage(3);
The output is as follows:
ConcreteHandler1::handleMessage()
Handling message 1
ConcreteHandler1::handleMessage()
Not handling message 2
ConcreteHandler2::handleMessage()
Handling message 2
ConcreteHandler1::handleMessage()
Not handling message 3
ConcreteHandler2::handleMessage()
Not handling message 3
The observer pattern is used to have objects/observers get notified by observable objects. With the observer pattern, individual objects register themselves with the observable object they are interested in. When the observable object’s state changes, it notifies all registered observers of this change.
The main benefit of using the observer pattern is that it decreases coupling. The observable class does not need to know the concrete observer types that are observing it. The observable class only needs to know about a basic interface, for example IObserver
.
First, an IObserver
interface is defined. Any object that wants to observe an observable should implement this interface:
class IObserver
{
public:
virtual ~IObserver() = default; // Always a virtual destructor!
virtual void notify() = 0;
};
Here are two concrete observers that simply print out a message in response to a notification:
class ConcreteObserver1 : public IObserver
{
public:
void notify() override
{
std::cout << "ConcreteObserver1::notify()" << std::endl;
}
};
class ConcreteObserver2 : public IObserver
{
public:
void notify() override
{
std::cout << "ConcreteObserver2::notify()" << std::endl;
}
};
An observable just keeps a list of IObserver
s that have registered themselves to get notified. It needs to support adding and removing observers, and should be able to notify all registered observers. All this functionality can be provided by an Observable
mixin class. Here is the implementation:
class Observable
{
public:
virtual ~Observable() = default; // Always a virtual destructor!
// Add an observer. Ownership is not transferred.
void addObserver(IObserver* observer)
{
mObservers.push_back(observer);
}
// Remove the given observer.
void removeObserver(IObserver* observer)
{
mObservers.erase(
std::remove(begin(mObservers), end(mObservers), observer),
end(mObservers));
}
protected:
void notifyAllObservers()
{
for (auto* observer : mObservers)
observer->notify();
}
private:
std::vector<IObserver*> mObservers;
};
A concrete class, ObservableSubject
, that wants to be observable simply derives from the Observable
mixin class to get all its functionality. Whenever the state of an ObservableSubject
changes, it simply calls notifyAllObservers()
to notify all registered observers.
class ObservableSubject : public Observable
{
public:
void modifyData()
{
// …
notifyAllObservers();
}
};
Following is a very simple test that demonstrates how to use the observer pattern.
ObservableSubject subject;
ConcreteObserver1 observer1;
subject.addObserver(&observer1);
subject.modifyData();
std::cout << std::endl;
ConcreteObserver2 observer2;
subject.addObserver(&observer2);
subject.modifyData();
The output is as follows:
ConcreteObserver1::notify()
ConcreteObserver1::notify()
ConcreteObserver2::notify()
This chapter has given you just a taste of how patterns can help you organize object-oriented concepts into high-level designs. A lot of design patterns are cataloged and discussed on Wikipedia1. It’s easy to get carried away and spend all your time trying to find the specific pattern that applies to your task. Instead, I recommend that you concentrate on a few patterns that interest you and focus your learning on how patterns are developed, not just the small differences between similar ones. After all, to paraphrase the old saying, “Teach me a design pattern, and I’ll code for a day. Teach me how to create design patterns, and I’ll code for a lifetime.”