C++ Design Patterns - A Comprehensive Guide
C++ Design Patterns - A Comprehensive Guide
Patterns
In 1994, the landscape of software development was fundamentally altered by the publication
of a single book: Design Patterns: Elements of Reusable Object-Oriented Software.1 Authored
by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides—a group that would
become famously known as the "Gang of Four" (GoF)—this work quickly became a
cornerstone of object-oriented design.3 It captured a wealth of experience by presenting a
catalog of 23 simple, elegant, and reusable solutions to commonly occurring problems in
software design.1 Even decades after its initial release, the book remains an Amazon
bestseller, a testament to its enduring impact on how developers structure, maintain, and
reason about their code.4 The patterns, originally demonstrated with examples in C++ and
Smalltalk, provided a systematic approach to creating flexible and robust software
architectures.1
A common misconception among beginners is that a design pattern is a rigid piece of code to
be copied directly into a project.5 In reality, a design pattern is more akin to a blueprint or a
template; it is a description of a proven solution to a recurring problem that can be adapted to
many different situations.5 The true power of design patterns lies not just in the solutions they
offer, but in the shared language they create among developers.7
The 23 GoF patterns are organized into three fundamental categories based on their purpose:
Creational, Structural, and Behavioral.4 This classification provides a framework for
understanding the distinct problem domains each pattern addresses.
● Creational Patterns: These patterns are concerned with the process of object creation.
Their primary goal is to abstract the instantiation process, making a system independent
of how its objects are created, composed, and represented. This increases flexibility by
decoupling the client from the specific classes it needs to instantiate.3
● Structural Patterns: These patterns describe how classes and objects can be composed
to form larger, more complex structures. They focus on simplifying the system's structure
by identifying relationships between entities, often using inheritance and composition to
create new functionalities.3
● Behavioral Patterns: These patterns are concerned with algorithms and the assignment
of responsibilities among objects. They describe how objects interact and communicate,
promoting loose coupling and flexibility in how these interactions are carried out.4
Before delving into the 23 GoF patterns, it is essential to understand the foundational
principles that underpin robust object-oriented design. The SOLID principles, an acronym for
five key design guidelines, are not patterns themselves but are abstract concepts that
motivate the structure of many patterns. A firm grasp of SOLID is a prerequisite for
appreciating why the GoF patterns are designed the way they are and for applying them
effectively.15
● Concept: The SRP states that "a class should have only one reason to change".11 This
means a class should have a single, well-defined responsibility. When a class has multiple
responsibilities, changes to one can inadvertently affect the others, leading to fragile
code. Adhering to SRP results in classes with high cohesion and greater robustness.17
● C++ Example: Consider a Journal class that manages a collection of entries but also
handles saving the journal to a file.
C++
// Violation of SRP
struct Journal {
std::string title;
std::vector<std::string> entries;
void add_entry(const std::string& entry);
// Persistence is a separate responsibility
void save(const std::string& filename) {
std::ofstream ofs(filename);
for (auto& e : entries) {
ofs << e << std::endl;
}
}
};
This class has two reasons to change: a change in how entries are managed, or a change
in how persistence is handled. To conform to SRP, these responsibilities should be
separated.
C++
// Conforming to SRP
struct Journal {
std::string title;
std::vector<std::string> entries;
void add_entry(const std::string& entry);
};
struct PersistenceManager {
static void save(const Journal& j, const std::string& filename) {
std::ofstream ofs(filename);
for (auto& e : j.entries) {
ofs << e << std::endl;
}
}
};
● Concept: The OCP states that "software entities... should be open for extension, but
closed for modification".11 This means you should be able to add new functionality to a
system without altering existing, tested code. This is typically achieved through
polymorphism and abstract interfaces.17
● C++ Example: Imagine a function that filters a list of products based on a specific
attribute, like color.
C++
// Violation of OCP
enum class Color { Red, Green, Blue };
struct Product { std::string name; Color color; };
struct ProductFilter {
std::vector<Product*> by_color(std::vector<Product*> items, Color color) {
std::vector<Product*> result;
for (auto& i : items) {
if (i->color == color) {
result.push_back(i);
}
}
return result;
}
};
If we need to add a filter for size, we would have to modify the ProductFilter class. A
better approach uses abstraction.
C++
// Conforming to OCP
template <typename T> struct Specification {
virtual bool is_satisfied(T* item) const = 0;
};
struct ColorSpecification : Specification<Product> {
Color color;
explicit ColorSpecification(const Color color) : color{color} {}
bool is_satisfied(Product* item) const override {
return item->color == color;
}
};
template <typename T> struct Filter {
virtual std::vector<T*> filter(std::vector<T*> items, const Specification<T>& spec) = 0;
};
struct BetterFilter : Filter<Product> {
std::vector<Product*> filter(std::vector<Product*> items, const Specification<Product>& spec)
override {
std::vector<Product*> result;
for (auto& item : items) {
if (spec.is_satisfied(item)) {
result.push_back(item);
}
}
return result;
}
};
Now, new filter criteria can be added by creating new Specification classes without
modifying BetterFilter.
● Concept: The LSP states that "functions that use pointers or references to base classes
must be able to use objects of derived classes without knowing it".19 In essence, a
subclass must be substitutable for its parent class without altering the correctness of the
program.11 This principle is about maintaining behavioral contracts.
● C++ Example: The classic example involves a Rectangle base class and a Square derived
class.
C++
class Rectangle {
protected:
int width, height;
public:
Rectangle(int w, int h) : width{w}, height{h} {}
virtual void set_width(int w) { width = w; }
virtual void set_height(int h) { height = h; }
int get_area() const { return width * height; }
};
// Violation of LSP
class Square : public Rectangle {
public:
Square(int size) : Rectangle{size, size} {}
void set_width(int w) override { this->width = this->height = w; }
void set_height(int h) override { this->width = this->height = h; }
};
void process(Rectangle& r) {
int w = 10;
r.set_width(w);
// For a Rectangle, we expect height to be unchanged.
// For a Square, set_width also changes the height, breaking the expectation.
}
A function process that works with a Rectangle& would produce unexpected behavior if a
Square is passed in, because the Square's setters modify both dimensions, violating the
implicit behavior of the Rectangle's setters. This indicates a flawed class hierarchy; a
square is not behaviorally a rectangle in this context.
● Concept: The ISP states that "clients should not be forced to depend upon interfaces
that they do not use".11 This principle advocates for creating smaller, more specific
interfaces rather than large, monolithic ones.
● C++ Example: Consider a single interface for a multi-function machine.
C++
// Violation of ISP
struct IMachine {
virtual void print(const std::string& doc) = 0;
virtual void scan(const std::string& doc) = 0;
virtual void fax(const std::string& doc) = 0;
};
struct SimplePrinter : IMachine {
void print(const std::string& doc) override { /*... */ }
void scan(const std::string& doc) override { /* empty, not applicable */ }
void fax(const std::string& doc) override { /* empty, not applicable */ }
};
The SimplePrinter is forced to implement scan and fax, methods it does not need. The
solution is to segregate the interface.
C++
// Conforming to ISP
struct IPrinter {
virtual void print(const std::string& doc) = 0;
};
struct IScanner {
virtual void scan(const std::string& doc) = 0;
};
struct Printer : IPrinter {
void print(const std::string& doc) override { /*... */ }
};
struct Photocopier : IPrinter, IScanner {
void print(const std::string& doc) override { /*... */ }
void scan(const std::string& doc) override { /*... */ }
};
● Concept: The DIP has two parts: 1) High-level modules should not depend on low-level
modules; both should depend on abstractions. 2) Abstractions should not depend on
details; details should depend on abstractions.11 This is the core principle of decoupling.
● C++ Example: A high-level Reporting module directly depends on a low-level
DatabaseLogger.
C++
// Violation of DIP
class DatabaseLogger {
public:
void log(const std::string& message) { /*... writes to DB... */ }
};
class Reporting {
DatabaseLogger logger; // Direct dependency on a concrete low-level module
public:
void generate_report() {
logger.log("Generating report...");
}
};
This couples Reporting to the DatabaseLogger. If we want to switch to a file logger, we
must modify the Reporting class. The solution is to introduce an abstraction.
C++
// Conforming to DIP
struct ILogger {
virtual ~ILogger() = default;
virtual void log(const std::string& message) = 0;
};
class DbLogger : public ILogger {
public:
void log(const std::string& message) override { /*... writes to DB... */ }
};
class Reporting {
ILogger& logger; // Depends on an abstraction
public:
Reporting(ILogger& logger) : logger{logger} {}
void generate_report() {
logger.log("Generating report...");
}
};
Now, the Reporting module depends on the ILogger interface, and any class that
implements this interface can be provided at runtime, effectively inverting the
dependency.
what gets created, who creates it, and how it gets created. Before a detailed examination of
each pattern, the following table provides a high-level overview to help differentiate their core
intents.
Intent
The Singleton pattern's intent is to ensure that a class has only one instance and to provide a
single, global point of access to that instance.6
Problem
The implementation of a Singleton class involves several key structural elements to enforce its
uniqueness 22:
● Private Constructor: Prevents direct instantiation of the class from outside code.
● Deleted Copy Constructor and Assignment Operator: Prevents clients from creating
copies of the single instance.
● Static Instance Member: A private static member to hold the one and only instance of
the class.
● Public Static Accessor Method: A public static method (commonly named
getInstance()) that provides the global access point. This method is responsible for
creating the instance on its first call and returning that same instance on all subsequent
calls.
C++ Implementation Deep Dive
A basic implementation illustrates the core logic but is unsafe in multithreaded environments.
C++
#include <iostream>
class Singleton {
private:
static Singleton* instance_;
Singleton() {
std::cout << "Singleton instance created.\n";
}
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* getInstance() {
if (instance_ == nullptr) {
instance_ = new Singleton();
}
return instance_;
}
};
Singleton* Singleton::instance_ = nullptr;
The primary issue here is the race condition within getInstance(). If two threads
simultaneously evaluate instance_ == nullptr as true, both will proceed to create a new
instance, violating the pattern's core guarantee.24
Since C++11, the language provides a simple, elegant, and thread-safe way to implement the
Singleton pattern using a static local variable. This is often called the "Meyers' Singleton."
C++
#include <iostream>
class Singleton {
private:
Singleton() {
std::cout << "Singleton instance created.\n";
}
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance() {
static Singleton instance; // Initialization is thread-safe since C++11
return instance;
}
void someBusinessLogic() {
std::cout << "Executing business logic.\n";
}
};
int main() {
Singleton& s1 = Singleton::getInstance();
Singleton& s2 = Singleton::getInstance();
s1.someBusinessLogic();
// s1 and s2 refer to the same object
return 0;
}
The C++ standard guarantees that the static Singleton instance; will be initialized only once,
even when getInstance() is called concurrently from multiple threads. This approach is the
preferred method in modern C++ due to its simplicity and guaranteed thread safety without
manual locking.25
Before C++11, a common technique was "double-checked locking" using mutexes. While it is
more complex and now largely unnecessary, it is a useful concept to understand.
C++
#include <iostream>
#include <mutex>
#include <atomic>
class Singleton {
private:
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
Singleton() {
std::cout << "Singleton instance created.\n";
}
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* getInstance() {
Singleton* p = instance_.load(std::memory_order_acquire);
if (p == nullptr) { // First check (without lock)
std::lock_guard<std::mutex> lock(mutex_);
p = instance_.load(std::memory_order_relaxed);
if (p == nullptr) { // Second check (with lock)
p = new Singleton();
instance_.store(p, std::memory_order_release);
}
}
return p;
}
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
This version uses an atomic pointer and a mutex to ensure thread safety. The first check
avoids the expensive lock acquisition if the instance already exists. The second check inside
the lock prevents a race condition if multiple threads pass the first check simultaneously.21
Consequences
● Pros:
○ Guaranteed Single Instance: Ensures only one object of a class is ever created.22
○ Global Access: Provides a convenient, globally accessible point to the instance.22
○ Lazy Initialization: The instance is created only when it is first requested.22
● Cons:
○ Global State: The pattern introduces global state into an application, which can
make code harder to reason about and debug. It creates hidden dependencies
between components.24
○ Violation of SRP: The Singleton class becomes responsible for both its core logic
and managing its own lifecycle.
○ Testing Difficulties: Tightly couples components to the concrete Singleton class,
making it difficult to substitute mock objects during unit testing.24
The prevalence of these drawbacks has led many experienced developers to consider the
Singleton an "anti-pattern".24 The need for a Singleton can often indicate a deeper design
issue, such as improper dependency management. In many cases, explicitly passing a shared
object via Dependency Injection leads to a more modular, testable, and maintainable design.
The Factory Method pattern defines an interface for creating an object but lets subclasses
decide which class to instantiate. It allows a class to defer the instantiation process to its
subclasses.6
Problem
Consider a generic framework for building applications, such as a document editor. The
framework might define an abstract Application class that knows how to create, open, and
save documents. It works with an abstract Document class. However, the framework itself
does not know what kind of concrete documents will be created—that is up to the specific
application built using the framework. For example, a text editor application will create
TextDocument objects, while a spreadsheet application will create Spreadsheet objects. The
framework's Application class should not be hardcoded to create TextDocuments, as this
would make it unusable for the spreadsheet application.
The Factory Method pattern elegantly solves this by delegating the creation decision to
subclasses.27
● Product: (Document) Defines the interface for objects the factory method creates.
● ConcreteProduct: (TextDocument, Spreadsheet) Implements the Product interface.
● Creator: (Application) Declares the factory method, which returns an object of type
Product. It may also define a default implementation. The Creator's primary role is often
its core business logic, which relies on the Product objects created by the factory
method.
● ConcreteCreator: (TextEditorApplication, SpreadsheetApplication) Overrides the factory
method to return an instance of a specific ConcreteProduct.
The following example implements the document editor framework scenario. It uses modern
C++ practices, such as std::unique_ptr for memory management and the override keyword for
clarity.
C++
#include <iostream>
#include <memory>
#include <vector>
#include <string>
// Product interface
class Document {
public:
virtual ~Document() = default;
virtual void open() = 0;
virtual void save() = 0;
};
// ConcreteProduct A
class TextDocument : public Document {
public:
void open() override { std::cout << "Opening Text Document.\n"; }
void save() override { std::cout << "Saving Text Document.\n"; }
};
// ConcreteProduct B
class Spreadsheet : public Document {
public:
void open() override { std::cout << "Opening Spreadsheet.\n"; }
void save() override { std::cout << "Saving Spreadsheet.\n"; }
};
// Creator
class Application {
protected:
std::vector<std::unique_ptr<Document>> docs_;
public:
virtual ~Application() = default;
// The Factory Method
virtual std::unique_ptr<Document> createDocument() const = 0;
void newDocument() {
std::unique_ptr<Document> doc = createDocument();
doc->open();
docs_.push_back(std::move(doc));
}
};
// ConcreteCreator A
class TextEditorApplication : public Application {
public:
std::unique_ptr<Document> createDocument() const override {
return std::make_unique<TextDocument>();
}
};
// ConcreteCreator B
class SpreadsheetApplication : public Application {
public:
std::unique_ptr<Document> createDocument() const override {
return std::make_unique<Spreadsheet>();
}
};
int main() {
std::unique_ptr<Application> app;
std::string config = "spreadsheet"; // This could come from a config file
if (config == "text") {
app = std::make_unique<TextEditorApplication>();
} else {
app = std::make_unique<SpreadsheetApplication>();
}
app->newDocument();
return 0;
}
In this example, the main function acts as the client. It decides at runtime which Application to
create. The Application's newDocument method then calls the createDocument factory
method. Because createDocument is virtual, the call is dispatched to the concrete subclass
(TextEditorApplication or SpreadsheetApplication), which creates the appropriate document
type. The base Application class remains completely decoupled from the concrete document
classes.30
Consequences
● Pros:
○ Loose Coupling: The Creator is not bound to concrete product classes, promoting
flexibility.30
○ Extensibility (OCP): New product types can be introduced by adding new
ConcreteProduct and ConcreteCreator classes without modifying existing client or
creator code.28
○ Centralized Creation Logic: The logic for creating an object is encapsulated within
the factory method, making the code cleaner and easier to maintain.30
● Cons:
○ Increased Complexity: The pattern can lead to a parallel hierarchy of classes, one
for products and one for creators, which can add complexity to the overall design.
Intent
The Abstract Factory pattern provides an interface for creating families of related or
dependent objects without specifying their concrete classes.6 It is often called a "factory of
factories" because it provides a way to group individual factory methods for a common
theme.3
Problem
Imagine developing a cross-platform application that must adapt its user interface (UI) to the
underlying operating system. For a consistent user experience, all UI elements—buttons,
scrollbars, windows—must match the OS's native look and feel. An application running on
Windows should create WindowsButton and WindowsScrollBar objects, while the same
application on macOS should create MacButton and MacScrollBar objects.
The challenge is to structure the application so that the client code that constructs the UI is
not littered with if-else or #ifdef statements to handle the different OS-specific classes.33 The
client should be able to create a whole family of widgets without being coupled to any
concrete widget class.
The Abstract Factory pattern solves this by defining an abstract factory for each family of
products.31
● AbstractFactory: (GUIFactory) Declares an interface with operations for creating each
type of abstract product (e.g., createButton(), createScrollBar()).
● ConcreteFactory: (WindowsFactory, MacFactory) Implements the operations to create
concrete product objects for a specific family.
● AbstractProduct: (Button, ScrollBar) Declares an interface for a type of product object.
● ConcreteProduct: (WindowsButton, MacButton, WindowsScrollBar, MacScrollBar)
Defines a product object to be created by the corresponding concrete factory and
implements the AbstractProduct interface.
● Client: Uses only the interfaces declared by AbstractFactory and AbstractProduct
classes.
This example implements the cross-platform UI toolkit scenario. The client code is configured
with a specific factory at runtime, and from that point on, it creates a consistent family of
products without knowing their concrete types.
C++
#include <iostream>
#include <memory>
#include <string>
// AbstractProduct A
class Button {
public:
virtual ~Button() = default;
virtual void paint() const = 0;
};
// ConcreteProduct A1
class WindowsButton : public Button {
public:
void paint() const override { std::cout << "Rendering a Windows button.\n"; }
};
// ConcreteProduct A2
class MacButton : public Button {
public:
void paint() const override { std::cout << "Rendering a macOS button.\n"; }
};
// AbstractProduct B
class ScrollBar {
public:
virtual ~ScrollBar() = default;
virtual void paint() const = 0;
};
// ConcreteProduct B1
class WindowsScrollBar : public ScrollBar {
public:
void paint() const override { std::cout << "Rendering a Windows scrollbar.\n"; }
};
// ConcreteProduct B2
class MacScrollBar : public ScrollBar {
public:
void paint() const override { std::cout << "Rendering a macOS scrollbar.\n"; }
};
// AbstractFactory
class GUIFactory {
public:
virtual ~GUIFactory() = default;
virtual std::unique_ptr<Button> createButton() const = 0;
virtual std::unique_ptr<ScrollBar> createScrollBar() const = 0;
};
// ConcreteFactory 1
class WindowsFactory : public GUIFactory {
public:
std::unique_ptr<Button> createButton() const override {
return std::make_unique<WindowsButton>();
}
std::unique_ptr<ScrollBar> createScrollBar() const override {
return std::make_unique<WindowsScrollBar>();
}
};
// ConcreteFactory 2
class MacFactory : public GUIFactory {
public:
std::unique_ptr<Button> createButton() const override {
return std::make_unique<MacButton>();
}
std::unique_ptr<ScrollBar> createScrollBar() const override {
return std::make_unique<MacScrollBar>();
}
};
// Client
class Application {
private:
std::unique_ptr<GUIFactory> factory_;
std::unique_ptr<Button> button_;
public:
Application(std::unique_ptr<GUIFactory> factory) : factory_(std::move(factory)) {}
void createUI() {
button_ = factory_->createButton();
}
void paint() {
button_->paint();
}
};
int main() {
// Configuration determines which factory to use
#ifdef _WIN32
auto factory = std::make_unique<WindowsFactory>();
#else
auto factory = std::make_unique<MacFactory>();
#endif
Application app(std::move(factory));
app.createUI();
app.paint();
return 0;
}
The client (Application) is initialized with a GUIFactory. It doesn't know or care if it's a
WindowsFactory or MacFactory. When it calls createButton(), the factory ensures the created
button is compatible with its family. This guarantees product consistency.31
Abstract Factory is often compared to Factory Method. A key difference is that Factory
Method uses class inheritance (subclasses decide which object to create), whereas Abstract
Factory uses object composition (the client is composed with a factory object that creates
the products). An Abstract Factory often uses Factory Methods in its implementation to create
the concrete products.
Chapter 5: Builder
Intent
The Builder pattern separates the construction of a complex object from its representation,
allowing the same construction process to create different variations of the object.8
Problem
Constructing an object can become unwieldy when it has a large number of parameters,
especially if many of them are optional. A common anti-pattern, the "telescoping constructor,"
arises where you have multiple constructor overloads with different combinations of
parameters. This leads to code that is hard to read and maintain.35 For example, creating an
HTML element might involve setting its tag, content, and numerous optional attributes.
C++
The Builder pattern provides a more flexible and readable solution by breaking down the
construction into a series of step-by-step method calls.
This example demonstrates building an HTML element using a fluent interface, where builder
methods return a reference to *this to enable method chaining.
C++
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
// Product
class HTMLElement {
public:
std::string name, text;
std::vector<std::pair<std::string, std::string>> attributes;
friend std::ostream& operator<<(std::ostream& os, const HTMLElement& e) {
os << "<" << e.name;
for (const auto& attr : e.attributes) {
os << " " << attr.first << "=\"" << attr.second << "\"";
}
os << ">" << e.text << "</" << e.name << ">";
return os;
}
};
// Builder
class HTMLBuilder {
protected:
HTMLElement root;
public:
HTMLBuilder(const std::string& root_name) {
root.name = root_name;
}
// Fluent interface for chaining
HTMLBuilder& add_child(const std::string& child_name, const std::string& child_text) {
// In a real implementation, this would build a tree.
// For simplicity, we'll just append to the root's text.
HTMLElement e{child_name, child_text};
std::stringstream ss;
ss << e;
root.text += ss.str();
return *this;
}
HTMLElement build() { return root; }
};
int main() {
HTMLBuilder builder{"ul"};
builder.add_child("li", "hello").add_child("li", "world");
HTMLElement list = builder.build();
std::cout << list << std::endl; // Output: <ul><li>hello</li><li>world</li></ul>
return 0;
}
Here, the HTMLBuilder acts as both the Builder and, in a simple way, the Director. The client
directly calls the construction steps (add_child). The chained calls
(builder.add_child(...).add_child(...)) provide a highly readable and expressive way to construct
the object.35
Consequences
● Pros:
○ Finer Control: The step-by-step construction process provides finer control over the
final product.
○ Improved Readability: Named methods for setting parameters are more readable
than a long list of constructor arguments.
○ Isolation of Logic: Construction logic is isolated from the product's business logic.39
○ Varying Representations: The same construction process can be used to create
different representations of the product by using different ConcreteBuilders.
● Cons:
○ Verbosity: The pattern requires creating a new Builder class for each type of
Product, which can increase the overall number of classes in the system.38
Chapter 6: Prototype
Intent
The Prototype pattern specifies the kinds of objects to create using a prototypical instance
and creates new objects by copying this prototype.8
Problem
This example uses a simple Shape hierarchy. We create prototype objects for a circle and a
rectangle, which can then be cloned to create new shape instances.
A critical aspect of implementing the clone() method in C++ is handling the difference
between a shallow copy and a deep copy. A shallow copy simply copies the values of an
object's members. If a member is a pointer, only the pointer address is copied, not the data it
points to. This can lead to multiple objects sharing and potentially corrupting the same
underlying data. A deep copy, on the other hand, creates new copies of any dynamically
allocated memory, ensuring that the clone is a completely independent object. When
implementing the Prototype pattern, a deep copy is almost always required.42
C++
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
// Prototype
class Shape {
public:
virtual ~Shape() = default;
virtual std::unique_ptr<Shape> clone() const = 0;
virtual void draw() const = 0;
};
// ConcretePrototype A
class Circle : public Shape {
private:
int radius;
public:
Circle(int r) : radius(r) {}
// Copy constructor for deep copy
Circle(const Circle& other) {
this->radius = other.radius;
}
std::unique_ptr<Shape> clone() const override {
return std::make_unique<Circle>(*this);
}
void draw() const override {
std::cout << "Drawing a circle with radius " << radius << ".\n";
}
};
// ConcretePrototype B
class Rectangle : public Shape {
private:
int width, height;
public:
Rectangle(int w, int h) : width(w), height(h) {}
std::unique_ptr<Shape> clone() const override {
return std::make_unique<Rectangle>(*this);
}
void draw() const override {
std::cout << "Drawing a rectangle with width " << width << " and height " << height << ".\n";
}
};
// Prototype Registry (optional but common)
class ShapeRegistry {
private:
std::unordered_map<std::string, std::unique_ptr<Shape>> prototypes;
public:
ShapeRegistry() {
prototypes["circle"] = std::make_unique<Circle>(10);
prototypes["rectangle"] = std::make_unique<Rectangle>(20, 15);
}
std::unique_ptr<Shape> createShape(const std::string& key) {
return prototypes[key]->clone();
}
};
int main() {
ShapeRegistry registry;
auto my_circle = registry.createShape("circle");
auto my_rectangle = registry.createShape("rectangle");
my_circle->draw();
my_rectangle->draw();
return 0;
}
The ShapeRegistry acts as a manager for the prototypes. The client code can request a new
shape by its key, and the registry finds the corresponding prototype and calls its clone()
method.42 The use of
Consequences
● Pros:
○ Performance: Can significantly improve performance by avoiding the cost of
repeated object creation from scratch.8
○ Flexibility: New concrete product classes can be added and registered with the
client at runtime without changing the client's code.
○ Simplicity: Hides the concrete product classes and their creation logic from the
client.
● Cons:
○ Complexity of Cloning: Implementing clone() can be complex for objects that have
circular references or complex internal structures.
○ Requires Deep Copy Implementation: Every class in the prototype hierarchy must
implement the clone() method and correctly handle deep copying.
Structural patterns focus on how classes and objects are composed to form larger, more
robust, and flexible structures.5 They are the architectural glue of a system, helping to
manage dependencies and simplify complex relationships. A key theme among these patterns
is the management of interfaces and object composition to reduce coupling and enhance
maintainability. For instance, the Adapter pattern is used reactively to make incompatible
interfaces work together, while the Bridge pattern is used proactively to prevent such
incompatibilities from arising in the first place. The Facade pattern simplifies a complex
subsystem by providing a single entry point, whereas the Proxy adds a layer of indirection to
control access to an object. Understanding these patterns provides a toolkit for shaping a
system's architecture effectively.
Chapter 7: Adapter
Intent
The Adapter pattern allows objects with incompatible interfaces to collaborate. It acts as a
wrapper that converts the interface of one class into an interface that another client
expects.20
Problem
Imagine integrating a new third-party analytics library into an existing application. The
application's code expects to work with an ILogger interface that has a
logMessage(std::string) method. However, the new analytics library provides a class
SuperAnalytics with a completely different interface, such as submitEvent(std::string
event_name, std::string event_data). Modifying either the existing application code or the
third-party library is undesirable or impossible.
This example demonstrates the Object Adapter pattern to make the SuperAnalytics library
work with the application's ILogger interface.
C++
#include <iostream>
#include <string>
#include <algorithm>
// Target interface
class ILogger {
public:
virtual ~ILogger() = default;
virtual void logMessage(const std::string& message) const = 0;
};
// Adaptee
class SuperAnalytics {
public:
void submitEvent(const std::string& event_name, const std::string& event_data) const {
std::cout << "SuperAnalytics: Event '" << event_name << "' with data '" << event_data << "'
submitted.\n";
}
};
// Adapter
class AnalyticsAdapter : public ILogger {
private:
SuperAnalytics* adaptee_;
public:
AnalyticsAdapter(SuperAnalytics* adaptee) : adaptee_(adaptee) {}
void logMessage(const std::string& message) const override {
// Translate the logMessage call into a submitEvent call
adaptee_->submitEvent("log", message);
}
};
// Client code
void clientCode(const ILogger& logger) {
logger.logMessage("This is a test message.");
}
int main() {
SuperAnalytics* analytics_service = new SuperAnalytics();
AnalyticsAdapter* adapter = new AnalyticsAdapter(analytics_service);
std::cout << "Client is using the adapter:\n";
clientCode(*adapter);
delete adapter;
delete analytics_service;
return 0;
}
The AnalyticsAdapter implements the ILogger interface. Its logMessage method takes the
simple string message and transforms it into the format expected by SuperAnalytics, calling
submitEvent on the wrapped adaptee_ object. The clientCode can now work with the analytics
service through the familiar ILogger interface, completely unaware of the adaptation
happening behind the scenes.46
Consequences
● Pros:
○ Reusability: Allows reuse of existing classes even if their interfaces do not match.46
○ Single Responsibility: The responsibility of interface conversion is isolated within
the Adapter class, keeping both the client and adaptee code clean.
○ Open/Closed Principle: New adapters can be introduced to support different
adaptees without modifying the client code.48
● Cons:
○ Increased Complexity: Introduces an additional layer of indirection and a new class,
which can add complexity to the system.
Chapter 8: Bridge
Intent
The Bridge pattern decouples an abstraction from its implementation so that the two can vary
independently.20 It involves splitting a large class or a set of closely related classes into two
separate hierarchies—Abstraction and Implementation.
Problem
Consider a set of geometric Shape classes (Circle, Square) that need to be rendered using
different drawing APIs (DrawingAPI_V1, DrawingAPI_V2). A naive approach would be to create
a subclass for each combination, such as Circle_V1, Circle_V2, Square_V1, and Square_V2. This
leads to a "Cartesian product" explosion of classes.50 If a new shape (
Triangle) or a new API (DrawingAPI_V3) is added, the number of classes grows exponentially,
making the system rigid and difficult to maintain.51
The Bridge pattern solves this by creating two independent inheritance hierarchies 49:
● Abstraction: (Shape) Defines the high-level interface that the client uses. It contains a
reference to an Implementation object.
● RefinedAbstraction: (Circle, Square) Extends the Abstraction interface to provide
specific variations.
● Implementation: (DrawingAPI) Defines the interface for the implementation classes. This
interface provides the primitive operations needed by the Abstraction.
● ConcreteImplementation: (DrawingAPI_V1, DrawingAPI_V2) Implements the
Implementation interface, providing platform-specific or API-specific code.
The key is that the Abstraction has-a Implementation (composition), rather than is-a
Implementation (inheritance).
This example implements the Shape and DrawingAPI scenario, demonstrating how the two
hierarchies can evolve independently.
C++
#include <iostream>
#include <memory>
// Implementation interface
class DrawingAPI {
public:
virtual ~DrawingAPI() = default;
virtual void drawCircle(double x, double y, double radius) = 0;
};
// ConcreteImplementation A
class DrawingAPI_V1 : public DrawingAPI {
public:
void drawCircle(double x, double y, double radius) override {
std::cout << "APIv1.circle at " << x << ":" << y << " radius " << radius << std::endl;
}
};
// ConcreteImplementation B
class DrawingAPI_V2 : public DrawingAPI {
public:
void drawCircle(double x, double y, double radius) override {
std::cout << "APIv2.circle at " << x << ":" << y << " radius " << radius << std::endl;
}
};
// Abstraction
class Shape {
protected:
DrawingAPI& drawingAPI;
public:
Shape(DrawingAPI& api) : drawingAPI(api) {}
virtual ~Shape() = default;
virtual void draw() = 0;
virtual void resize(double pct) = 0;
};
// RefinedAbstraction
class CircleShape : public Shape {
private:
double x, y, radius;
public:
CircleShape(double x, double y, double r, DrawingAPI& api) : Shape(api), x(x), y(y), radius(r) {}
void draw() override {
drawingAPI.drawCircle(x, y, radius);
}
void resize(double pct) override {
radius *= pct;
}
};
int main() {
DrawingAPI_V1 api1;
DrawingAPI_V2 api2;
CircleShape circle1(1, 2, 3, api1);
CircleShape circle2(5, 7, 11, api2);
circle1.draw();
circle2.draw();
return 0;
}
Here, Shape is the abstraction and DrawingAPI is the implementation. A CircleShape can be
constructed with any object that conforms to the DrawingAPI interface. We can now add new
shapes (e.g., SquareShape) or new drawing APIs (e.g., OpenGL_API) without affecting the
other hierarchy.52
Bridge is often confused with Adapter, but they have different intents. Adapter is used
retrospectively to make incompatible interfaces work together. Bridge is designed up-front to
allow the abstraction and implementation to vary independently.45 The structure of Bridge is
similar to State and Strategy, as all are based on composition; however, they solve different
problems.51
Chapter 9: Composite
Intent
The Composite pattern composes objects into tree structures to represent part-whole
hierarchies. It lets clients treat individual objects (leaves) and compositions of objects
(composites) uniformly.20
Problem
Many applications require dealing with objects that are structured as a tree. For example, a
graphics application needs to handle simple shapes like lines and circles (primitives) as well as
complex shapes that are groups of other shapes (composites). The client code should be able
to perform operations like draw() or move() on any graphical object, regardless of whether it
is a simple primitive or a complex group, without needing complex if-else logic to differentiate
between them.
The pattern achieves this uniform treatment through a shared interface 54:
● Component: (Graphic) The abstract base class or interface for all objects in the
composition. It declares the interface for operations common to all components, such as
draw(). It may also declare an interface for managing child components (add, remove).
● Leaf: (Circle, Line) Represents the primitive objects in the composition. A Leaf has no
children, so its add and remove methods are typically empty.
● Composite: (Picture) Represents a composite object that can have children. It stores
child components (usually in a list or vector) and implements the child-management
operations. Its implementation of draw() typically iterates over its children and calls
draw() on each of them.
● Client: Manipulates objects in the composition through the Component interface.
C++
#include <iostream>
#include <vector>
#include <memory>
#include <string>
#include <algorithm>
// Component
class Graphic {
public:
virtual ~Graphic() = default;
virtual void draw() const = 0;
virtual void add(Graphic* g) {} // Default empty implementation
virtual void remove(Graphic* g) {}
};
// Leaf
class Circle : public Graphic {
public:
void draw() const override {
std::cout << "Drawing a Circle.\n";
}
};
// Composite
class Picture : public Graphic {
private:
std::vector<Graphic*> children;
public:
~Picture() {
for (Graphic* child : children) {
delete child;
}
}
void draw() const override {
std::cout << "Drawing a Picture:\n";
for (const auto& child : children) {
child->draw();
}
}
void add(Graphic* g) override {
children.push_back(g);
}
void remove(Graphic* g) override {
children.erase(std::remove(children.begin(), children.end(), g), children.end());
}
};
int main() {
// Client code works with all components via the base interface
Picture* main_pic = new Picture();
main_pic->add(new Circle());
Picture* sub_pic = new Picture();
sub_pic->add(new Circle());
sub_pic->add(new Circle());
main_pic->add(sub_pic);
main_pic->draw();
delete main_pic;
return 0;
}
The client code can build a complex tree structure (main_pic containing a Circle and a
sub_pic, which itself contains two Circles) and then call draw() on the root of the tree. The
Picture::draw() method recursively calls draw() on all its children, demonstrating the uniform
treatment of both leaf (Circle) and composite (Picture) objects.57
Consequences
● Pros:
○ Uniformity: Client code is simplified because it doesn't need to distinguish between
simple and composite objects.
○ Extensibility: It is easy to add new kinds of Component classes (either new leaves or
new composites).
○ Hierarchical Structures: The pattern makes it easy to work with complex, recursive
tree structures.
● Cons:
○ "Fat" Interface: The Component interface can become overly general. Leaf classes
are forced to have methods like add and remove that they don't use.
○ Type Safety: It can be difficult to restrict the types of components that can be
added to a composite at compile time.
Intent
The Decorator pattern allows for adding new behaviors or responsibilities to an object
dynamically by placing it inside a special wrapper object, called a decorator.20 This provides a
flexible alternative to subclassing for extending functionality.5
Problem
Imagine a text editor application with a TextView component that displays text. You need to
add features like borders and scrollbars. One approach is to use inheritance, creating
subclasses like TextViewWithBorder, TextViewWithScrollbar, and even
TextViewWithBorderAndScrollbar. This leads to a class explosion, where each combination of
features requires a new subclass. Furthermore, this approach is static; features are added at
compile time and cannot be changed at runtime.
This example implements the TextView scenario, showing how decorators can be "stacked" on
top of each other.
C++
#include <iostream>
#include <memory>
#include <string>
// Component
class VisualComponent {
public:
virtual ~VisualComponent() = default;
virtual void draw() const = 0;
};
// ConcreteComponent
class TextView : public VisualComponent {
public:
void draw() const override {
std::cout << "Drawing TextView content.";
}
};
// Decorator base class
class Decorator : public VisualComponent {
protected:
std::unique_ptr<VisualComponent> component_;
public:
Decorator(std::unique_ptr<VisualComponent> component) :
component_(std::move(component)) {}
void draw() const override {
if (component_) {
component_->draw();
}
}
};
// ConcreteDecorator A
class BorderDecorator : public Decorator {
public:
BorderDecorator(std::unique_ptr<VisualComponent> component) :
Decorator(std::move(component)) {}
void draw() const override {
std::cout << "";
}
};
// ConcreteDecorator B
class ScrollDecorator : public Decorator {
public:
ScrollDecorator(std::unique_ptr<VisualComponent> component) :
Decorator(std::move(component)) {}
void draw() const override {
std::cout << "<Scrollbar: ";
Decorator::draw();
std::cout << ">";
}
};
int main() {
// Create a TextView
auto textView = std::make_unique<TextView>();
// Decorate it with a border
auto borderedView = std::make_unique<BorderDecorator>(std::move(textView));
// Decorate the bordered view with a scrollbar
auto scrolledAndBorderedView =
std::make_unique<ScrollDecorator>(std::move(borderedView));
std::cout << "Drawing the fully decorated component:\n";
scrolledAndBorderedView->draw();
std::cout << "\n";
return 0;
}
In main, a TextView is first wrapped by a BorderDecorator, and then the resulting object is
wrapped by a ScrollDecorator. When draw() is called on the outermost decorator, the call is
passed down the chain. ScrollDecorator::draw() calls BorderDecorator::draw(), which in turn
calls TextView::draw(). Each decorator adds its own behavior around the delegated call,
resulting in a "stacked" output: <Scrollbar:>.58
Consequences
● Pros:
○ Flexibility: New features can be added to objects at runtime, and multiple decorators
can be combined.60
○ Avoids Feature-Laden Superclasses: Functionality is divided among several
smaller decorator classes instead of being concentrated in a single large class.
○ Composition over Inheritance: The pattern favors a more flexible object
composition model over a rigid static inheritance model.60
● Cons:
○ Many Small Objects: A design using Decorator can result in a system with a large
number of small, similar-looking objects, which can be hard to debug and
understand.59
○ Complexity: The initial setup with multiple layers of wrapping can be more complex
than simple inheritance.
Chapter 11: Facade
Intent
The Facade pattern provides a simplified, unified interface to a complex subsystem of classes,
a library, or a framework. It defines a higher-level interface that makes the subsystem easier
to use.20
Problem
Modern software systems often rely on complex libraries and frameworks with dozens of
classes and intricate dependencies. To perform a simple task, a client might need to initialize
multiple objects, manage their lifecycles, and call their methods in a specific order.64 This
tightly couples the client code to the subsystem's internal implementation details, making the
client code complex and difficult to maintain. If the subsystem is updated or replaced, the
client code must be extensively rewritten.
The Facade pattern introduces a single class to shield the client from this complexity 62:
● Facade: This class knows which subsystem classes are responsible for a request and
delegates client requests to the appropriate subsystem objects. It provides a simple,
high-level interface for common tasks.
● Subsystem Classes: These classes implement the low-level functionality of the
subsystem. They have no knowledge of the Facade and are not aware that they are part
of a larger system from the Facade's perspective.
● Client: Communicates with the subsystem by sending requests to the Facade, rather
than interacting with the subsystem classes directly.
C++ Implementation Deep Dive
This example models a simplified home theater system. To watch a movie, the client would
normally have to interact with the Amplifier, DvdPlayer, and Projector classes individually. The
HomeTheaterFacade simplifies this process into a single watchMovie() call.
C++
#include <iostream>
#include <string>
#include <memory>
// Subsystem classes
class Amplifier {
public:
void on() { std::cout << "Amplifier on\n"; }
void setDvd() { std::cout << "Amplifier setting DVD player\n"; }
void setVolume(int level) { std::cout << "Amplifier setting volume to " << level << "\n"; }
void off() { std::cout << "Amplifier off\n"; }
};
class DvdPlayer {
public:
void on() { std::cout << "DVD Player on\n"; }
void play(const std::string& movie) { std::cout << "DVD Player playing \"" << movie << "\"\n"; }
void off() { std::cout << "DVD Player off\n"; }
};
class Projector {
public:
void on() { std::cout << "Projector on\n"; }
void wideScreenMode() { std::cout << "Projector in widescreen mode\n"; }
void off() { std::cout << "Projector off\n"; }
};
// Facade
class HomeTheaterFacade {
private:
std::shared_ptr<Amplifier> amp;
std::shared_ptr<DvdPlayer> dvd;
std::shared_ptr<Projector> projector;
public:
HomeTheaterFacade(std::shared_ptr<Amplifier> a, std::shared_ptr<DvdPlayer> d,
std::shared_ptr<Projector> p)
: amp(a), dvd(d), projector(p) {}
void watchMovie(const std::string& movie) {
std::cout << "Get ready to watch a movie...\n";
projector->on();
projector->wideScreenMode();
amp->on();
amp->setDvd();
amp->setVolume(5);
dvd->on();
dvd->play(movie);
}
void endMovie() {
std::cout << "\nShutting movie theater down...\n";
dvd->off();
amp->off();
projector->off();
}
};
int main() {
auto amp = std::make_shared<Amplifier>();
auto dvd = std::make_shared<DvdPlayer>();
auto projector = std::make_shared<Projector>();
HomeTheaterFacade homeTheater(amp, dvd, projector);
homeTheater.watchMovie("Raiders of the Lost Ark");
homeTheater.endMovie();
return 0;
}
The HomeTheaterFacade encapsulates all the steps required to start and stop the movie. The
client code in main is now much simpler and is decoupled from the individual subsystem
components. It only needs to interact with the facade.65
Consequences
● Pros:
○ Simplified Interface: Provides a simple entry point to a complex system, making it
easier to use.65
○ Decoupling: Decouples clients from the subsystem's components, which promotes
modularity and allows the subsystem to be modified or replaced with minimal impact
on the client code.65
○ Layering: Helps to structure a system into layers, with the Facade acting as the entry
point to each layer.64
● Cons:
○ Potential God Object: A Facade can become a "god object" coupled to all classes
of an application if not designed carefully.
○ Limited Functionality: The Facade provides a simplified interface, which may not
expose all the functionality of the subsystem. Clients who need more advanced
features may still need to access the subsystem classes directly.
Intent
The Flyweight pattern allows a program to support vast quantities of objects by minimizing
memory consumption. It achieves this by sharing common parts of an object's state between
multiple objects, rather than storing all data in each object.20
Problem
Some applications require creating a huge number of similar objects, which can lead to
excessive memory usage. For example, in a word processor, each character has properties
like font, size, and style, but also a position on the page. If every single character object
stored its font data, the memory footprint would be enormous, as most characters in a
document share the same font.
The Flyweight pattern addresses this by separating an object's state into two parts 67:
● Intrinsic State: This is the data that is constant and can be shared among many objects
(e.g., the font, size, and style of a character). This state is stored within the Flyweight
object.
● Extrinsic State: This is the data that is unique to each object and cannot be shared (e.g.,
the character's position and the character code itself). This state is passed to the
Flyweight's methods by the client.
This example models the rendering of trees in a forest. Each Tree has a unique position (x, y),
which is its extrinsic state. However, the TreeType (name, color, texture) is shared among
many trees and represents the intrinsic state.
C++
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <memory>
// The Flyweight class containing the intrinsic state
class TreeType {
private:
std::string name;
std::string color;
public:
TreeType(const std::string& n, const std::string& c) : name(n), color(c) {}
void draw(int x, int y) const {
std::cout << "Drawing a " << color << " " << name << " tree at (" << x << ", " << y << ").\n";
}
};
// The Flyweight Factory
class TreeFactory {
private:
std::unordered_map<std::string, std::shared_ptr<TreeType>> treeTypes;
public:
std::shared_ptr<TreeType> getTreeType(const std::string& name, const std::string& color) {
std::string key = name + "_" + color;
if (treeTypes.find(key) == treeTypes.end()) {
std::cout << "Creating new TreeType: " << key << "\n";
treeTypes[key] = std::make_shared<TreeType>(name, color);
}
return treeTypes[key];
}
};
// The Context class that uses the flyweights
class Tree {
private:
int x, y; // Extrinsic state
std::shared_ptr<TreeType> type; // Pointer to the Flyweight
public:
Tree(int x, int y, std::shared_ptr<TreeType> type) : x(x), y(y), type(type) {}
void draw() const {
type->draw(x, y);
}
};
// Client
class Forest {
private:
TreeFactory factory;
std::vector<Tree> trees;
public:
void plantTree(int x, int y, const std::string& name, const std::string& color) {
auto type = factory.getTreeType(name, color);
trees.emplace_back(x, y, type);
}
void draw() const {
for (const auto& tree : trees) {
tree.draw();
}
}
};
int main() {
Forest forest;
forest.plantTree(10, 20, "Oak", "Green");
forest.plantTree(50, 30, "Pine", "Dark Green");
forest.plantTree(100, 80, "Oak", "Green"); // Reuses existing TreeType
forest.draw();
return 0;
}
The TreeFactory ensures that for every unique combination of name and color, only one
TreeType object is created. The Forest plants multiple Tree objects, each with its own
coordinates, but they share TreeType objects. When the third tree ("Oak", "Green") is planted,
the factory recognizes that this type already exists and returns the shared instance, saving
memory.68
Consequences
● Pros:
○ Memory Efficiency: Can drastically reduce the number of objects in memory,
especially when there are many instances with shared state.68
○ Performance: Reusing objects can be faster than creating new ones.
● Cons:
○ Increased Complexity: The code becomes more complex due to the separation of
intrinsic and extrinsic state and the need for a factory to manage the flyweight
objects.68
○ Runtime Cost: There is a small runtime cost associated with retrieving or computing
the extrinsic state.
Intent
The Proxy pattern provides a surrogate or placeholder for another object to control access to
it. This allows for additional logic to be executed before or after the request is forwarded to
the original object.20
Problem
There are several scenarios where direct access to an object is not desirable or practical.
● Lazy Initialization (Virtual Proxy): An object might be very resource-intensive to create
(e.g., loading a high-resolution image from disk). We may want to delay its creation until it
is actually needed.69
● Access Control (Protection Proxy): We may want to control access to an object based
on the client's permissions. For example, some users may have read-only access while
others have read-write access.70
● Remote Access (Remote Proxy): The object may reside in a different address space or
on a remote server. The proxy acts as a local representative, handling the network
communication.
● Logging/Caching: The proxy can log requests to the object or cache the results of
expensive operations.
Structure and Participants
The Proxy pattern ensures that the proxy and the real object share the same interface, making
them interchangeable from the client's perspective.72
● Subject: An interface that is common to both the RealSubject and the Proxy. This allows
the client to work with the Proxy as if it were the RealSubject.
● RealSubject: The actual object that the proxy represents. It contains the core business
logic.
● Proxy: Maintains a reference to the RealSubject. It implements the Subject interface and
can perform additional tasks (like access control or lazy loading) before or after
forwarding the request to the RealSubject.
● Client: Interacts with the Subject interface, unaware of whether it is communicating with
the RealSubject or a Proxy.
C++
#include <iostream>
#include <string>
// Subject interface
class IFolder {
public:
virtual ~IFolder() = default;
virtual void performReadWrite() = 0;
};
// RealSubject
class SharedFolder : public IFolder {
public:
void performReadWrite() override {
std::cout << "Performing read/write operations on the shared folder.\n";
}
};
// User class for demonstrating access control
class User {
public:
std::string username;
std::string role;
};
// Proxy
class FolderProxy : public IFolder {
private:
std::unique_ptr<SharedFolder> real_folder_;
User user_;
public:
FolderProxy(const User& user) : user_(user) {}
void performReadWrite() override {
if (user_.role == "Admin" |
| user_.role == "Developer") {
// Lazy initialization
if (!real_folder_) {
real_folder_ = std::make_unique<SharedFolder>();
}
std::cout << "Proxy: Access granted for user " << user_.username << ".\n";
real_folder_->performReadWrite();
} else {
std::cout << "Proxy: Access denied for user " << user_.username << ". Insufficient
privileges.\n";
}
}
};
int main() {
User admin_user{"AdminUser", "Admin"};
User guest_user{"GuestUser", "Guest"};
FolderProxy admin_proxy(admin_user);
FolderProxy guest_proxy(guest_user);
std::cout << "Client trying to access with admin credentials:\n";
admin_proxy.performReadWrite();
std::cout << "\nClient trying to access with guest credentials:\n";
guest_proxy.performReadWrite();
return 0;
}
The FolderProxy checks the User's role before granting access. If the user has the appropriate
role, it creates the SharedFolder object (if it doesn't exist already) and delegates the call. If
not, it denies access. The client interacts with both proxies through the IFolder interface, but
the behavior changes based on the access control logic within the proxy.73
Consequences
● Pros:
○ Controlled Access: The proxy can control access to the real object, adding a layer
of security or management.
○ Lazy Initialization: Can improve performance by delaying the creation of expensive
objects.
○ Separation of Concerns: Additional functionalities like logging, caching, or
networking are handled by the proxy, keeping the RealSubject focused on its core
business logic.
● Cons:
○ Increased Indirection: The additional layer of the proxy can add complexity and a
slight performance overhead to every call.
○ Interface Duplication: The proxy must mirror the interface of the RealSubject, which
can lead to code duplication if the interface is large.
Behavioral patterns are concerned with algorithms and the assignment of responsibilities
between objects. They describe patterns of communication, making interactions flexible and
loosely coupled.4 A key theme is the encapsulation of behavior in objects, allowing that
behavior to be changed or composed dynamically. For example, the State and Strategy
patterns are structurally similar, both using composition to alter an object's behavior. However,
their intents differ: Strategy focuses on
how an object performs a task (the algorithm), often selected by the client, while State
focuses on what an object does based on its internal condition, with state transitions often
managed internally. Similarly, the Command and Memento patterns often work in synergy;
Command encapsulates a request, and Memento can be used to save the state before the
command is executed, enabling a robust undo/redo mechanism.
Intent
The Chain of Responsibility pattern avoids coupling the sender of a request to its receiver by
giving more than one object a chance to handle the request. The receiving objects are
chained together, and the request is passed along the chain until an object handles it.20
Problem
Imagine an order processing system where an order request must pass through several
validation steps: authentication, authorization, data validation, and inventory check. These
checks must be performed sequentially, and if any check fails, the process should stop.
Hardcoding this sequence of checks in a single large method makes the system rigid. Adding,
removing, or reordering checks requires modifying this method, violating the Open/Closed
Principle.75
The pattern decouples the sender from the handlers by linking the handler objects into a
chain 74:
● Handler: Defines an interface for handling requests. It typically includes a reference to
the next handler in the chain.
● ConcreteHandler: Implements the handling logic. It decides whether it can process the
request. If it can, it does so; otherwise, it passes the request to the next handler in the
chain.
● Client: Initiates the request and sends it to the first handler in the chain.
This example models a system for approving purchase requests. Different manager levels
(TeamLead, Manager, Director) can approve requests up to a certain amount.
C++
#include <iostream>
#include <string>
#include <memory>
class PurchaseRequest {
public:
double amount;
std::string purpose;
};
// Handler interface
class Approver {
protected:
std::shared_ptr<Approver> next_approver_;
public:
virtual ~Approver() = default;
void setNext(std::shared_ptr<Approver> next) {
next_approver_ = next;
}
virtual void processRequest(const PurchaseRequest& request) = 0;
};
// ConcreteHandler A
class TeamLead : public Approver {
public:
void processRequest(const PurchaseRequest& request) override {
if (request.amount <= 500) {
std::cout << "Team Lead approved purchase of " << request.amount << " for " <<
request.purpose << ".\n";
} else if (next_approver_!= nullptr) {
next_approver_->processRequest(request);
} else {
std::cout << "No one can approve this request.\n";
}
}
};
// ConcreteHandler B
class Manager : public Approver {
public:
void processRequest(const PurchaseRequest& request) override {
if (request.amount > 500 && request.amount <= 5000) {
std::cout << "Manager approved purchase of " << request.amount << " for " <<
request.purpose << ".\n";
} else if (next_approver_!= nullptr) {
next_approver_->processRequest(request);
}
}
};
// ConcreteHandler C
class Director : public Approver {
public:
void processRequest(const PurchaseRequest& request) override {
if (request.amount > 5000) {
std::cout << "Director approved purchase of " << request.amount << " for " <<
request.purpose << ".\n";
} else if (next_approver_!= nullptr) {
next_approver_->processRequest(request);
}
}
};
int main() {
auto lead = std::make_shared<TeamLead>();
auto manager = std::make_shared<Manager>();
auto director = std::make_shared<Director>();
// Build the chain
lead->setNext(manager);
manager->setNext(director);
PurchaseRequest req1{300, "New Keyboard"};
PurchaseRequest req2{4500, "Team Server"};
PurchaseRequest req3{12000, "Cloud Subscription"};
lead->processRequest(req1);
lead->processRequest(req2);
lead->processRequest(req3);
return 0;
}
The client constructs the chain of approvers: lead -> manager -> director. When a request is
sent to the lead, it either handles it or passes it on. This allows the chain to be reconfigured
dynamically without changing the handler classes themselves.77
Consequences
● Pros:
○ Reduced Coupling: The sender of a request does not need to know which object will
handle it.
○ Flexibility: The chain can be modified at runtime by adding or removing handlers.
○ Single Responsibility: Each handler is responsible for a specific part of the
processing logic.
● Cons:
○ Request Not Guaranteed to be Handled: A request might travel to the end of the
chain without being processed if no handler is configured to handle it.75
○ Debugging: Tracing the flow of a request through a long chain can be difficult.
Intent
The Command pattern encapsulates a request as an object, thereby letting you parameterize
clients with different requests, queue or log requests, and support undoable operations.6
Problem
In many applications, especially those with graphical user interfaces, you need to perform
actions in response to user input. For example, a button click might save a document, a menu
item might copy text, and a keyboard shortcut might paste text. A naive implementation would
couple the UI elements (the "invokers") directly to the business logic objects (the "receivers").
This makes the UI components less reusable and the code harder to maintain, as the logic is
scattered.79
The Command pattern decouples the invoker from the receiver by introducing a command
object 78:
● Command: Declares an interface for executing an operation, typically a single method
like execute().
● ConcreteCommand: Implements the Command interface. It holds a reference to a
Receiver object and invokes one or more of its methods. It also stores any arguments
required for the receiver's methods.
● Invoker: (Button, MenuItem) Asks the command to carry out the request. The invoker is
not aware of the receiver or the specifics of the operation.
● Receiver: (Document, TextBuffer) Knows how to perform the operations associated with
carrying out a request. It contains the actual business logic.
● Client: Creates a ConcreteCommand object and sets its receiver.
#include <iostream>
#include <memory>
#include <vector>
// Receiver
class Light {
public:
void turnOn() { std::cout << "The light is ON\n"; }
void turnOff() { std::cout << "The light is OFF\n"; }
};
// Command interface
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
virtual void undo() = 0;
};
// ConcreteCommand
class LightOnCommand : public Command {
private:
Light& light_;
public:
LightOnCommand(Light& light) : light_(light) {}
void execute() override { light_.turnOn(); }
void undo() override { light_.turnOff(); }
};
class LightOffCommand : public Command {
private:
Light& light_;
public:
LightOffCommand(Light& light) : light_(light) {}
void execute() override { light_.turnOff(); }
void undo() override { light_.turnOn(); }
};
// Invoker
class RemoteControl {
private:
std::shared_ptr<Command> command_;
public:
void setCommand(std::shared_ptr<Command> cmd) {
command_ = cmd;
}
void pressButton() {
if (command_) command_->execute();
}
void pressUndo() {
if (command_) command_->undo();
}
};
int main() {
Light livingRoomLight;
auto lightOn = std::make_shared<LightOnCommand>(livingRoomLight);
auto lightOff = std::make_shared<LightOffCommand>(livingRoomLight);
RemoteControl remote;
remote.setCommand(lightOn);
remote.pressButton(); // The light is ON
remote.pressUndo(); // The light is OFF
remote.setCommand(lightOff);
remote.pressButton(); // The light is OFF
remote.pressUndo(); // The light is ON
return 0;
}
Consequences
● Pros:
○ Decoupling: Decouples the object that invokes the operation from the one that
knows how to perform it.
○ Extensibility: New commands can be added easily without changing existing code.
○ Advanced Operations: Commands are first-class objects, so they can be stored,
queued, logged, or sent over a network. This enables features like macro recording
and undo/redo.
● Cons:
○ Increased Complexity: Can result in a proliferation of small command classes for
every action in the application.
Intent
Given a language, the Interpreter pattern defines a representation for its grammar along with
an interpreter that uses this representation to interpret sentences in the language.20
Problem
Some applications require processing a simple language or expression format. For example, a
search engine might need to parse queries like "design AND (pattern OR C++)", or a financial
application might need to evaluate mathematical formulas. Implementing a parser and
interpreter for such a language can be complex. The Interpreter pattern provides a structured
way to solve this problem using an object-oriented approach.
The pattern represents the grammar as a class hierarchy, often resembling a Composite
structure 81:
● AbstractExpression: Declares an abstract interpret() method that is common to all
nodes in the Abstract Syntax Tree (AST).
● TerminalExpression: Implements the interpret() method for terminal symbols in the
grammar (e.g., numbers, variables). These are the leaves of the AST.
● NonterminalExpression: Implements the interpret() method for non-terminal symbols
(e.g., addition, subtraction, logical operators). These expressions are composites that
contain other AbstractExpression objects.
● Context: Contains information that is global to the interpreter, such as a symbol table.
● Client: Builds (or is given) an AST representing a sentence in the language and invokes
the interpret() method.
This example implements a simple interpreter for arithmetic expressions involving addition
and subtraction of integers.
C++
#include <iostream>
#include <string>
#include <map>
#include <memory>
// Context
using Context = std::map<std::string, int>;
// AbstractExpression
class Expression {
public:
virtual ~Expression() = default;
virtual int interpret(const Context& context) const = 0;
};
// TerminalExpression
class Number : public Expression {
private:
int number;
public:
Number(int num) : number(num) {}
int interpret(const Context& context) const override {
return number;
}
};
// NonterminalExpression
class Plus : public Expression {
private:
std::shared_ptr<Expression> left;
std::shared_ptr<Expression> right;
public:
Plus(std::shared_ptr<Expression> l, std::shared_ptr<Expression> r) : left(l), right(r) {}
int interpret(const Context& context) const override {
return left->interpret(context) + right->interpret(context);
}
};
class Minus : public Expression {
private:
std::shared_ptr<Expression> left;
std::shared_ptr<Expression> right;
public:
Minus(std::shared_ptr<Expression> l, std::shared_ptr<Expression> r) : left(l), right(r) {}
int interpret(const Context& context) const override {
return left->interpret(context) - right->interpret(context);
}
};
int main() {
// Represents the expression: 5 + (10 - 2)
auto expression = std::make_shared<Plus>(
std::make_shared<Number>(5),
std::make_shared<Minus>(
std::make_shared<Number>(10),
std::make_shared<Number>(2)
)
);
Context context; // Context is empty for this simple example
std::cout << "Result: " << expression->interpret(context) << std::endl; // Output: 13
return 0;
}
The client code manually builds the AST for the expression 5 + (10 - 2). Calling interpret() on
the root node triggers a recursive evaluation of the entire tree, resulting in the final value.83
Consequences
● Pros:
○ Extensibility: The grammar is easy to extend. New rules can be added by creating
new Expression subclasses.
○ Modularity: Each grammar rule is represented by its own class, making the code
organized and maintainable.82
● Cons:
○ Complexity for Large Grammars: The pattern is not practical for complex
grammars, as the number of classes can become unmanageable. For complex
languages, using a parser generator (like ANTLR or Bison) is a better approach.
○ Limited Applicability: The pattern is best suited for simple, well-defined
grammars.82
Intent
The Iterator pattern provides a way to access the elements of an aggregate object (a
collection) sequentially without exposing its underlying representation (e.g., list, vector,
tree).20
Problem
A collection class, such as a custom list or tree, should provide a way for clients to traverse its
elements. A naive approach might be to expose the internal data structure (e.g., return a
reference to the internal std::vector). This violates encapsulation and couples the client
directly to the collection's implementation. If the internal data structure changes (e.g., from a
std::vector to a std::list), all client code that traverses the collection will break.
The Iterator pattern solves this by encapsulating the traversal logic in a separate iterator
object 85:
● Iterator: An interface that defines the operations for traversal, such as next(), hasNext(),
and currentItem().
● ConcreteIterator: Implements the Iterator interface and keeps track of the current
position in the traversal of a specific Aggregate.
● Aggregate: An interface that defines a factory method for creating an Iterator object
(e.g., createIterator()).
● ConcreteAggregate: Implements the Aggregate interface and returns an instance of a
ConcreteIterator that can traverse its elements.
While the C++ Standard Library provides a powerful and standardized iterator concept,
implementing the pattern from scratch is instructive. This example creates a custom
NumberCollection and a corresponding NumberIterator.
C++
#include <iostream>
#include <vector>
#include <stdexcept>
// Forward declaration
class NumberCollection;
// Iterator interface
class Iterator {
public:
virtual ~Iterator() = default;
virtual int next() = 0;
virtual bool hasNext() const = 0;
};
// Aggregate interface
class Aggregate {
public:
virtual ~Aggregate() = default;
virtual std::unique_ptr<Iterator> createIterator() = 0;
};
// ConcreteIterator
class NumberIterator : public Iterator {
private:
NumberCollection& collection_;
size_t index_;
public:
NumberIterator(NumberCollection& collection);
int next() override;
bool hasNext() const override;
};
// ConcreteAggregate
class NumberCollection : public Aggregate {
private:
std::vector<int> numbers_;
friend class NumberIterator;
public:
void add(int number) {
numbers_.push_back(number);
}
std::unique_ptr<Iterator> createIterator() override {
return std::make_unique<NumberIterator>(*this);
}
};
// Implementation of NumberIterator methods after NumberCollection is fully defined
NumberIterator::NumberIterator(NumberCollection& collection) : collection_(collection),
index_(0) {}
int NumberIterator::next() {
if (!hasNext()) {
throw std::out_of_range("No more elements");
}
return collection_.numbers_[index_++];
}
bool NumberIterator::hasNext() const {
return index_ < collection_.numbers_.size();
}
int main() {
NumberCollection collection;
collection.add(10);
collection.add(20);
collection.add(30);
auto it = collection.createIterator();
while (it->hasNext()) {
std::cout << it->next() << std::endl;
}
return 0;
}
The client obtains an iterator from the NumberCollection and uses its hasNext() and next()
methods to traverse the elements. The client is completely unaware that the collection is
internally using a std::vector. The collection's implementation could be changed to a std::list or
another data structure, and only the NumberCollection and NumberIterator classes would
need to be updated; the client code would remain unchanged.87
Consequences
● Pros:
○ Encapsulation: Hides the internal structure of the collection from the client.
○ Multiple Traversals: Allows multiple iterators to traverse the same collection
independently and simultaneously.
○ Uniform Interface: Provides a consistent interface for traversing different types of
collections.
● Cons:
○ Overkill for Simple Collections: For simple collections, implementing the full
pattern might be more complex than necessary, especially when language-provided
iterators are available.
Intent
The Mediator pattern defines an object that encapsulates how a set of objects interact. It
promotes loose coupling by keeping objects from referring to each other explicitly, and it lets
you vary their interaction independently.20
Problem
In systems with many components, such as a GUI dialog with multiple widgets (buttons, text
boxes, checkboxes), the communication logic can become a tangled mess. Each widget might
need to know about and communicate with several other widgets. For example, typing in a
text box might enable a button, which, when clicked, updates a list box. This creates a
"many-to-many" web of dependencies, making the individual widget classes hard to reuse
and the overall logic difficult to understand and maintain.88
The Mediator pattern solves this by centralizing the communication logic 89:
● Mediator: Defines an interface for communicating with Colleague objects.
● ConcreteMediator: Implements the communication logic and coordinates the Colleague
objects. It knows and maintains its colleagues.
● Colleague: Defines an interface for communicating with the Mediator.
● ConcreteColleague: Each colleague class knows its Mediator object. It communicates
with its mediator whenever it would have otherwise communicated with another
colleague.
This example models a chat room where User objects communicate through a central
ChatMediator instead of directly with each other.
C++
#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <algorithm>
class Colleague; // Forward declaration
// Mediator interface
class Mediator {
public:
virtual ~Mediator() = default;
virtual void sendMessage(const std::string& message, Colleague* sender) = 0;
};
// Colleague base class
class Colleague {
protected:
Mediator* mediator_;
public:
Colleague(Mediator* mediator) : mediator_(mediator) {}
virtual ~Colleague() = default;
};
// ConcreteColleague
class User : public Colleague {
private:
std::string name_;
public:
User(const std::string& name, Mediator* mediator) : Colleague(mediator), name_(name) {}
const std::string& getName() const { return name_; }
void send(const std::string& message) {
std::cout << name_ << " sends: " << message << "\n";
mediator_->sendMessage(message, this);
}
void receive(const std::string& message) {
std::cout << name_ << " receives: " << message << "\n";
}
};
// ConcreteMediator
class ChatMediator : public Mediator {
private:
std::vector<User*> users_;
public:
void addUser(User* user) {
users_.push_back(user);
}
void sendMessage(const std::string& message, Colleague* sender) override {
User* senderUser = static_cast<User*>(sender);
for (User* user : users_) {
if (user!= senderUser) {
user->receive(message);
}
}
}
};
int main() {
ChatMediator mediator;
User alice("Alice", &mediator);
User bob("Bob", &mediator);
User charlie("Charlie", &mediator);
mediator.addUser(&alice);
mediator.addUser(&bob);
mediator.addUser(&charlie);
alice.send("Hi everyone!");
return 0;
}
When alice.send() is called, the User object doesn't send the message to Bob and Charlie
directly. Instead, it notifies the ChatMediator. The mediator then iterates through its list of
users and forwards the message to everyone except the original sender. The User objects are
completely decoupled from each other; they only know about the Mediator.91
Consequences
● Pros:
○ Reduced Coupling: Decouples colleagues, reducing dependencies and making
them easier to reuse and modify independently.89
○ Centralized Control: The interaction logic is centralized in one place, making it
easier to understand and maintain.
○ Simplifies Object Protocols: Replaces complex many-to-many interactions with
simpler one-to-many interactions (from each colleague to the mediator).
● Cons:
○ Mediator God Object: The ConcreteMediator can become overly complex and
monolithic if it tries to manage too many colleagues and interactions.
Intent
The Memento pattern, without violating encapsulation, captures and externalizes an object's
internal state so that the object can be restored to this state later.20
Problem
Sometimes it is necessary to record the internal state of an object at a particular moment and
restore it later. This is the foundation of undo/redo functionality, database transactions, and
"save game" features.93 The challenge is to get the state out of the object without exposing its
internal implementation details, which would violate encapsulation.
The Memento pattern solves this with three key roles 92:
● Originator: The object whose state needs to be saved. It creates a Memento containing a
snapshot of its current state and uses a Memento to restore its state.
● Memento: A passive object that stores the internal state of the Originator. It should
protect against access by objects other than the originator. In C++, this can be achieved
using the friend keyword.
● Caretaker: Is responsible for the memento's safekeeping but never operates on or
examines its contents. It requests a memento from the originator to save a state and
passes a memento back to the originator to restore a state.
This example implements a simple text editor that can save and restore its content.
C++
#include <iostream>
#include <string>
#include <vector>
#include <memory>
// Memento
class EditorMemento {
private:
std::string content_;
// Make Originator a friend to allow it to access the private state
friend class TextEditor;
EditorMemento(const std::string& content) : content_(content) {}
};
// Originator
class TextEditor {
private:
std::string content_;
public:
void type(const std::string& text) {
content_ += text;
}
const std::string& getContent() const {
return content_;
}
std::shared_ptr<EditorMemento> save() {
return std::make_shared<EditorMemento>(content_);
}
void restore(std::shared_ptr<EditorMemento> memento) {
if (memento) {
content_ = memento->content_;
}
}
};
// Caretaker
class History {
private:
std::vector<std::shared_ptr<EditorMemento>> mementos_;
std::shared_ptr<TextEditor> editor_;
public:
History(std::shared_ptr<TextEditor> editor) : editor_(editor) {}
void backup() {
mementos_.push_back(editor_->save());
}
void undo() {
if (mementos_.empty()) return;
auto memento = mementos_.back();
mementos_.pop_back();
editor_->restore(memento);
}
};
int main() {
auto editor = std::make_shared<TextEditor>();
History history(editor);
history.backup();
editor->type("Hello, ");
std::cout << editor->getContent() << std::endl;
history.backup();
editor->type("World!");
std::cout << editor->getContent() << std::endl;
history.undo();
std::cout << "After undo: " << editor->getContent() << std::endl;
return 0;
}
The EditorMemento's constructor and state are private. By declaring TextEditor as a friend,
only the TextEditor (the Originator) can create mementos and access their internal state to
restore itself. The History (Caretaker) holds a list of EditorMemento objects but cannot access
their private content_ member, thus preserving the TextEditor's encapsulation.95
Consequences
● Pros:
○ Preserves Encapsulation: The state is saved without exposing the internal structure
of the Originator.
○ Simplifies Originator: The Originator doesn't need to manage its history; this logic is
handled by the Caretaker.
○ Enables Undo/Redo: Provides a clean mechanism for implementing state restoration
features.93
● Cons:
○ Memory Consumption: Storing many mementos can be memory-intensive,
especially if the Originator's state is large.
○ Language Dependency: The ability to restrict access to the memento's state may
depend on language features (like friend in C++).
Intent
The Observer pattern defines a one-to-many dependency between objects so that when one
object (the subject) changes state, all its dependents (observers) are notified and updated
automatically.6
Problem
In many systems, a change in one object requires other objects to be updated. For example, in
a spreadsheet, when the value of one cell changes, all other cells that use its value in their
formulas must be recalculated. A naive implementation might hardcode the cell
dependencies, but this creates tight coupling. The cell that changes (the subject) would need
explicit knowledge of all the cells that depend on it (the observers), making the system
difficult to maintain and extend.
The Observer pattern decouples the subject from its observers 97:
● Subject: Knows its observers and provides an interface for attaching and detaching
Observer objects.
● Observer: Defines an updating interface for objects that should be notified of changes in
a subject.
● ConcreteSubject: Stores state of interest to ConcreteObserver objects and sends a
notification to its observers when its state changes.
● ConcreteObserver: Maintains a reference to a ConcreteSubject object and implements
the Observer updating interface to keep its state consistent with the subject's.
C++ Implementation Deep Dive
This example models a weather station (WeatherStation) that notifies various displays
(CurrentConditionsDisplay) when weather data changes.
C++
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <memory>
// Observer interface
class IObserver {
public:
virtual ~IObserver() = default;
virtual void update(float temp, float humidity) = 0;
};
// Subject interface
class ISubject {
public:
virtual ~ISubject() = default;
virtual void registerObserver(IObserver* o) = 0;
virtual void removeObserver(IObserver* o) = 0;
virtual void notifyObservers() = 0;
};
// ConcreteSubject
class WeatherStation : public ISubject {
private:
std::vector<IObserver*> observers;
float temperature;
float humidity;
public:
void registerObserver(IObserver* o) override {
observers.push_back(o);
}
void removeObserver(IObserver* o) override {
observers.erase(std::remove(observers.begin(), observers.end(), o), observers.end());
}
void notifyObservers() override {
for (IObserver* observer : observers) {
observer->update(temperature, humidity);
}
}
void setMeasurements(float temp, float hum) {
this->temperature = temp;
this->humidity = hum;
notifyObservers();
}
};
// ConcreteObserver
class CurrentConditionsDisplay : public IObserver {
public:
void update(float temp, float humidity) override {
std::cout << "Current conditions: " << temp << "F degrees and " << humidity << "% humidity\n";
}
};
int main() {
WeatherStation weatherStation;
CurrentConditionsDisplay display1;
weatherStation.registerObserver(&display1);
weatherStation.setMeasurements(80, 65);
weatherStation.setMeasurements(82, 70);
return 0;
}
Consequences
● Pros:
○ Loose Coupling: The subject and observers are loosely coupled. The subject only
knows that its observers implement the IObserver interface.99
○ Dynamic Relationships: Observers can be added or removed at any time.
○ Broadcast Communication: Supports a one-to-many notification model.
● Cons:
○ Unexpected Updates: Observers are notified of all changes, even if they are not
interested in every change. This can lead to inefficient "update storms".99
○ Update Order: The order in which observers are notified is not guaranteed.
Intent
The State pattern allows an object to alter its behavior when its internal state changes. The
object will appear to change its class.20
Problem
An object's behavior often depends on its current state. For example, a Document object
might behave differently depending on whether it is in a Draft, Moderation, or Published state.
A common but poor way to implement this is with a large switch or if-else statement inside the
object's methods, checking the current state and executing the appropriate behavior. This
approach is problematic because it violates the Single Responsibility and Open/Closed
principles. The method becomes bloated with state-specific logic, and adding a new state
requires modifying this large conditional block.
The State pattern solves this by encapsulating the state-specific behaviors into separate
classes 101:
● Context: (Document) The object whose behavior changes with its state. It maintains an
instance of a ConcreteState subclass that defines the current state.
● State: An interface that encapsulates the behavior associated with a particular state of
the Context.
● ConcreteState: (DraftState, PublishedState) Each subclass implements a behavior
associated with a state of the Context. They can also be responsible for transitioning the
Context to a new state.
This example models a simple traffic light system. The TrafficLight (Context) delegates its
change() method to its current State object.
C++
#include <iostream>
#include <memory>
#include <typeinfo>
class TrafficLight; // Forward declaration
// State interface
class LightState {
public:
virtual ~LightState() = default;
virtual void change(TrafficLight& light) = 0;
virtual void reportState() const = 0;
};
// Context
class TrafficLight {
private:
std::unique_ptr<LightState> state_;
public:
TrafficLight(std::unique_ptr<LightState> initial_state);
void transitionTo(std::unique_ptr<LightState> state) {
state_ = std::move(state);
std::cout << "Traffic light is now ";
state_->reportState();
std::cout << ".\n";
}
void requestChange() {
state_->change(*this);
}
};
// ConcreteStates
class RedLight : public LightState {
public:
void change(TrafficLight& light) override;
void reportState() const override { std::cout << "RED"; }
};
class GreenLight : public LightState {
public:
void change(TrafficLight& light) override;
void reportState() const override { std::cout << "GREEN"; }
};
class YellowLight : public LightState {
public:
void change(TrafficLight& light) override;
void reportState() const override { std::cout << "YELLOW"; }
};
// Implement state transitions after all classes are defined
void RedLight::change(TrafficLight& light) {
light.transitionTo(std::make_unique<GreenLight>());
}
void GreenLight::change(TrafficLight& light) {
light.transitionTo(std::make_unique<YellowLight>());
}
void YellowLight::change(TrafficLight& light) {
light.transitionTo(std::make_unique<RedLight>());
}
// Context constructor
TrafficLight::TrafficLight(std::unique_ptr<LightState> initial_state) {
transitionTo(std::move(initial_state));
}
int main() {
TrafficLight light(std::make_unique<RedLight>());
light.requestChange(); // -> Green
light.requestChange(); // -> Yellow
light.requestChange(); // -> Red
return 0;
}
The TrafficLight's behavior for requestChange() is entirely determined by its current state_.
The logic for state transitions is encapsulated within the ConcreteState classes themselves
(e.g., RedLight::change transitions the context to GreenLight). This eliminates the need for
large conditional statements in the TrafficLight class and makes it easy to add new states
without modifying existing code.
The State and Strategy patterns are structurally very similar, both relying on composition to
delegate behavior to another object. The key difference lies in their intent. Strategy is typically
used to provide different algorithms for a single task, and the client often chooses the
strategy. State is used to manage an object's behavior as its internal state changes, and the
state transitions are often managed by the context or the state objects themselves.103
Chapter 22: Strategy
Intent
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them
interchangeable. Strategy lets the algorithm vary independently from clients that use it.6
Problem
An application may need to perform a specific task in different ways. For example, a
navigation app might need to calculate a route for a car, a bicycle, or for walking. A data
compression utility might need to use different compression algorithms (ZIP, RAR, 7z).
Hardcoding these algorithms into the main class using a large switch statement makes the
class difficult to maintain. Adding a new algorithm requires modifying the class, violating the
Open/Closed Principle.
The Strategy pattern extracts these algorithms into separate classes called "strategies".103
● Strategy: An interface common to all supported algorithms.
● ConcreteStrategy: Implements a specific algorithm using the Strategy interface.
● Context: Is configured with a ConcreteStrategy object and maintains a reference to it. It
communicates with the strategy object via the Strategy interface. The Context is not
aware of the concrete type of strategy it is using.
This example shows a Sorter class that can be configured with different sorting algorithms at
runtime.
C++
#include <iostream>
#include <vector>
#include <algorithm>
#include <memory>
// Strategy interface
class SortStrategy {
public:
virtual ~SortStrategy() = default;
virtual void sort(std::vector<int>& data) const = 0;
};
// ConcreteStrategy A
class BubbleSortStrategy : public SortStrategy {
public:
void sort(std::vector<int>& data) const override {
std::cout << "Sorting using Bubble Sort.\n";
// Simplified bubble sort logic
for (size_t i = 0; i < data.size(); ++i) {
for (size_t j = 0; j < data.size() - 1; ++j) {
if (data[j] > data[j+1]) {
std::swap(data[j], data[j+1]);
}
}
}
}
};
// ConcreteStrategy B
class QuickSortStrategy : public SortStrategy {
public:
void sort(std::vector<int>& data) const override {
std::cout << "Sorting using Quick Sort.\n";
std::sort(data.begin(), data.end()); // Using std::sort for simplicity
}
};
// Context
class Sorter {
private:
std::unique_ptr<SortStrategy> strategy_;
public:
Sorter(std::unique_ptr<SortStrategy> strategy) : strategy_(std::move(strategy)) {}
void set_strategy(std::unique_ptr<SortStrategy> strategy) {
strategy_ = std::move(strategy);
}
void performSort(std::vector<int>& data) {
if (strategy_) {
strategy_->sort(data);
}
}
};
int main() {
std::vector<int> data = {5, 1, 4, 2, 8};
Sorter sorter(std::make_unique<BubbleSortStrategy>());
sorter.performSort(data);
data = {5, 1, 4, 2, 8}; // Reset data
sorter.set_strategy(std::make_unique<QuickSortStrategy>());
sorter.performSort(data);
return 0;
}
The Sorter (Context) is initialized with a sorting strategy. The client can change this strategy at
any time using set_strategy. The Sorter's performSort method simply delegates the work to
the current strategy object. This allows the sorting algorithm to be selected and changed
dynamically.
A modern C++ approach for simpler, stateless strategies is to use std::function, which can
wrap any callable object, including free functions, lambdas, or functors, avoiding the need for
a full class hierarchy.100
Consequences
● Pros:
○ Interchangeable Algorithms: You can swap algorithms at runtime.
○ Isolation: The implementation details of an algorithm are isolated from the client
code that uses it.
○ Composition over Inheritance: Avoids coupling the context to specific algorithms
through inheritance.
○ Open/Closed Principle: New strategies can be introduced without modifying the
context.
● Cons:
○ Increased Objects: The pattern increases the number of objects in the application.
○ Client Awareness: The client must be aware of the different strategies to select the
appropriate one.
Intent
The Template Method pattern defines the skeleton of an algorithm in a base class but lets
subclasses override specific steps of the algorithm without changing its structure.20
Problem
Suppose you are developing a data processing framework. The overall process for handling
different data files (e.g., CSV, JSON) is the same: open the file, extract the data, process the
data, and close the file. While the overall structure of this algorithm is constant, the specific
implementation of opening, extracting, and processing the data will differ for each file format.
Placing the entire algorithm in each subclass would lead to significant code duplication for
the common steps.
Structure and Participants
The Template Method pattern solves this by defining the algorithm's structure in a base
class.104
● AbstractClass: Defines the "template method," which calls a series of primitive
operations. It also defines the abstract primitive operations that concrete subclasses
must implement. It can also define "hooks," which are optional steps with a default (often
empty) implementation that subclasses can override.
● ConcreteClass: Implements the primitive operations to carry out the subclass-specific
steps of the algorithm.
This example implements a simple beverage-making process. The overall algorithm for making
tea and coffee is similar (boil water, brew, pour in cup, add condiments), but the specific
brewing and condiment steps differ.
C++
#include <iostream>
// AbstractClass
class CaffeineBeverage {
public:
// The Template Method
void prepareRecipe() final {
boilWater();
brew();
pourInCup();
if (customerWantsCondiments()) {
addCondiments();
}
}
protected:
// Primitive operations to be implemented by subclasses
virtual void brew() = 0;
virtual void addCondiments() = 0;
// Concrete operations shared by all subclasses
void boilWater() {
std::cout << "Boiling water\n";
}
void pourInCup() {
std::cout << "Pouring into cup\n";
}
// A "hook" method with a default implementation
virtual bool customerWantsCondiments() {
return true;
}
};
// ConcreteClass A
class Tea : public CaffeineBeverage {
protected:
void brew() override {
std::cout << "Steeping the tea\n";
}
void addCondiments() override {
std::cout << "Adding lemon\n";
}
};
// ConcreteClass B
class Coffee : public CaffeineBeverage {
protected:
void brew() override {
std::cout << "Dripping coffee through filter\n";
}
void addCondiments() override {
std::cout << "Adding sugar and milk\n";
}
// Overriding the hook
bool customerWantsCondiments() override {
return false;
}
};
int main() {
Tea myTea;
std::cout << "Making tea...\n";
myTea.prepareRecipe();
std::cout << "\nMaking coffee...\n";
Coffee myCoffee;
myCoffee.prepareRecipe();
return 0;
}
The prepareRecipe() method is the template method. It is marked final to prevent subclasses
from overriding the algorithm's structure. It calls the abstract brew() and addCondiments()
methods, which are implemented by Tea and Coffee to provide their specific behaviors. The
customerWantsCondiments() method is a hook that allows subclasses to optionally alter the
algorithm's flow.
Intent
Consider a complex object structure, such as an Abstract Syntax Tree (AST) representing
code, or a document composed of different elements like paragraphs and images. You may
need to perform various unrelated operations on this structure, such as printing,
type-checking, or exporting to XML. Adding a new virtual function for each new operation to
the base Node class is not feasible. It would clutter the interface and violate the Open/Closed
Principle, as every new operation would require modifying all node classes.106
The Visitor pattern solves this by separating the operations from the object structure 105:
● Visitor: An interface that declares a visit() method for each class of ConcreteElement in
the object structure. The method signature, e.g., visit(ConcreteElementA*), allows the
visitor to know the concrete type of the element it is visiting.
● ConcreteVisitor: Implements the Visitor interface, providing the specific algorithm for
each ConcreteElement.
● Element: An interface that declares an accept() method that takes a visitor as an
argument.
● ConcreteElement: Implements the accept() method. Its implementation is simple: it calls
the visit() method on the visitor, passing itself (this) as the argument.
● ObjectStructure: A collection of Element objects (e.g., a Composite). It provides a way
to iterate over its elements and allow a visitor to visit them.
This technique is known as "double dispatch." The first dispatch is the call to accept(), which
resolves to the concrete element's type. The second dispatch is the call to visit(), which
resolves to the concrete visitor's type and the concrete element's type (via method
overloading).
This example shows how to apply different operations (visitors) to a hierarchy of geometric
shapes.
C++
#include <iostream>
#include <vector>
#include <memory>
// Forward declare elements for the Visitor interface
class Circle;
class Square;
// Visitor interface
class ShapeVisitor {
public:
virtual ~ShapeVisitor() = default;
virtual void visit(const Circle& circle) = 0;
virtual void visit(const Square& square) = 0;
};
// Element interface
class Shape {
public:
virtual ~Shape() = default;
virtual void accept(ShapeVisitor& visitor) const = 0;
};
// ConcreteElements
class Circle : public Shape {
public:
void accept(ShapeVisitor& visitor) const override {
visitor.visit(*this);
}
};
class Square : public Shape {
public:
void accept(ShapeVisitor& visitor) const override {
visitor.visit(*this);
}
};
// ConcreteVisitor A
class DrawVisitor : public ShapeVisitor {
public:
void visit(const Circle& circle) override {
std::cout << "Drawing a circle.\n";
}
void visit(const Square& square) override {
std::cout << "Drawing a square.\n";
}
};
// ConcreteVisitor B
class XMLExportVisitor : public ShapeVisitor {
public:
void visit(const Circle& circle) override {
std::cout << "<circle/>\n";
}
void visit(const Square& square) override {
std::cout << "<square/>\n";
}
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>());
shapes.push_back(std::make_unique<Square>());
DrawVisitor drawer;
XMLExportVisitor exporter;
std::cout << "Drawing all shapes:\n";
for (const auto& shape : shapes) {
shape->accept(drawer);
}
std::cout << "\nExporting all shapes to XML:\n";
for (const auto& shape : shapes) {
shape->accept(exporter);
}
return 0;
}
The client creates a collection of Shape objects. To draw them, it creates a DrawVisitor and
passes it to each shape's accept method. To export them, it creates an XMLExportVisitor and
does the same. New operations can be added by creating new visitor classes without ever
touching the Shape, Circle, or Square classes.107
Consequences
● Pros:
○ Open/Closed Principle: New operations can be added easily by creating new visitor
classes.
○ Separation of Concerns: Related operations are grouped together in a single visitor
class, rather than being scattered across the element hierarchy.
○ Visits Complex Structures: Visitors can accumulate state as they traverse a
complex object structure (like a Composite).
● Cons:
○ Breaks Encapsulation: The visitor often needs access to the internal state of the
elements, which may require making their members public.
○ Difficult to Add New Elements: Adding a new ConcreteElement class is difficult, as
it requires updating the Visitor interface and all ConcreteVisitor classes to add a new
visit method. This pattern is best used when the element hierarchy is stable.
The Gang of Four patterns were cataloged in an era dominated by C++98 and Smalltalk. While
their underlying principles remain timeless, the advent of modern C++ (C++11 and beyond)
has profoundly changed how these patterns are implemented and, in some cases, whether
they are needed at all.1 Modern C++ features like smart pointers, move semantics, lambda
expressions, and advanced template metaprogramming offer more elegant, safer, and often
more performant ways to solve the same problems.
Works cited