29
Applying Design Patterns

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

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.

c29-fig-0001

FIGURE 29-1

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 PATTERN

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.

Example: A Logging Mechanism

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:

  • It is available at all times.
  • It is easy to use.
  • There is only one instance.

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!

Implementation of a Singleton

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:

  • It can log a single string or a vector of strings.
  • Each log message has an associated log level, which is prefixed to the log message.
  • The logger can be set up to only log messages of a certain log level.
  • Every logged message is flushed to disk so that it will appear in the file immediately.

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);
    }
}

image 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";
};

Using a Singleton

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.

THE ABSTRACT FACTORY PATTERN

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.

Example: A Car Factory Simulation

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 Cars without knowing whether they were really Toyotas or Fords. 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.

c29-fig-0002

FIGURE 29-2

c29-fig-0003

FIGURE 29-3

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.

c29-fig-0004

FIGURE 29-4

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.

c29-fig-0005

FIGURE 29-5

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.

Implementation of a Factory

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.

Using a Factory

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.

Other Uses of Factories

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

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.

Example: Hiding Network Connectivity Issues

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.

Implementation of a Proxy

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.";
}

Using a Proxy

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 ADAPTOR PATTERN

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.

Example: Adapting a Logger Class

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.

Implementation of an Adaptor

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());
}

Using an Adaptor

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

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).

Example: Defining Styles in Web Pages

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.

c29-fig-0006

FIGURE 29-6

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.

c29-fig-0007

FIGURE 29-7

Implementation of a Decorator

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;
};

Using a Decorator

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>

THE CHAIN OF RESPONSIBILITY PATTERN

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.

Example: Event Handling

Consider a drawing program, which has a hierarchy of Shape classes, as in Figure 29-8.

c29-fig-0008

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.

Implementation of a Chain of Responsibility

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.

Chain of Responsibility without Hierarchy

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

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.

Implementation of an Observer

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;
        }
};

Implementation of an Observable

An observable just keeps a list of IObservers 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();
        }
};

Using an Observer

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()

SUMMARY

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.”

NOTE

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset