C++ Design Patterns

Introduction

In the development process, as the amount of project code increases, the difficulty of maintenance and extension gradually rises. How to design the structure of the code to decouple it, facilitate extension and maintenance, and ease team collaboration is a significant challenge. Design patterns were proposed to address this issue. The design patterns built on design principles provide templates for code structure design in various application scenarios. To discuss design patterns, one must also consider design principles. This article analyzes and introduces commonly used design patterns from three parts: design principles, UML class diagrams, and design patterns, explaining the main applicable scenarios for each pattern based on their characteristics.

C++ Design Patterns

1 Design Principles

Design principles are the fundamental starting point for design patterns, and design patterns are the intuitive embodiment of design principles. To deeply understand each design pattern, one must start from design principles, analyze the specific problems they solve, the basic ideas behind their design, and the applicable scenarios. To discuss design patterns, one must first understand design principles.

1.1 Single Responsibility Principle (Class Construction Principle)

The Single Responsibility Principle (SRP) states that a class should have only one reason to change. If there are multiple motivations to change a class, then the class has more than one responsibility, and the extra responsibilities should be separated out into different classes. For example, if a person has multiple roles that are not closely related and may even conflict, they will not be able to effectively resolve these responsibilities, and these should be distributed among different individuals. The Single Responsibility Principle is the best method for achieving high cohesion and low coupling.

1.2 Open-Closed Principle (Function Extension Principle)

The Open-Closed Principle (OCP) states that a software entity such as a class, module, or function should be open for extension but closed for modification. The goal is to ensure that the program is easily extensible, maintainable, and upgradable. The Open-Closed Principle is considered the cornerstone of object-oriented design; in fact, other principles can be seen as tools and means to achieve the Open-Closed Principle. This means that software should be open to extension but closed to modification. In simple terms, when developing software, one should focus on extending its functionality without modifying the original program. The benefit is that the software is highly flexible and extensible. When new functionality is needed, new modules can be added to meet the new requirements. Additionally, since the original modules are not modified, there is no need to worry about stability issues.

1.3 Dependency Inversion Principle (Calling Rules)

The Dependency Inversion Principle (DIP) is a rule for calling between classes. Here, dependency refers to coupling in the code. High-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions. Interface programming is primarily based on the idea that if a member or parameter in a class becomes a concrete type, then that class depends on that concrete type. If in an inheritance structure, a member or parameter in a high-level class is of a lower-level type, then the high-level class depends on the low-level class, which should be avoided by focusing on abstractions or interface programming. For example, if there is a Driver class with a member that is a Car object and a driver() method, and the Car object has two methods start() and stop(), it is clear that Driver depends on Car, meaning the Driver class calls methods from the Car class. However, when support for a Bus class is added (the driver needs to drive a bus), the code in Driver must be changed, violating the Open-Closed Principle. The root cause is that the high-level Driver class is tightly coupled with the low-level Car class. One solution is to abstract the Car and Bus classes and introduce an abstract class called Automobile. Car and Bus would then be generalizations of Automobile. After this modification, the original high-level dependency on the low-level class is transformed into both high-level and low-level classes depending on the abstraction. This is the essence of the Dependency Inversion Principle.

1.4 Liskov Substitution Principle

The Liskov Substitution Principle states that subclasses can extend the functionality of their parent class but cannot change the original functionality of the parent class. In the Open-Closed Principle, it advocates for “abstraction” and “polymorphism.” Maintaining the encapsulation of design through “abstraction” is a feature provided by the language, while “polymorphism” is achieved through inheritance semantics. Therefore, how do we measure the quality of the inheritance relationship? The answer is that inheritance must ensure that the properties possessed by the superclass (parent class) still hold in the subclass. In object-oriented thinking, an object is a combination of a set of states and a series of behaviors. The state is the intrinsic characteristic of the object, while the behavior is the extrinsic characteristic. The LSP states that the formations within the same inheritance system should have common behavioral characteristics.

1.5 Law of Demeter

The Law of Demeter (Least Knowledge Principle) states that an object should have the least knowledge about other objects. In simple terms, a class should know the least about the classes it needs to couple with or call. No matter how complex your class is internally, that is your business; I only know that you have so many public methods that I can call. The Law of Demeter does not want direct contact between classes. If there really needs to be a connection, it should be conveyed through their friend classes. For example, if you need to buy a house, and there are three suitable properties A, B, and C, you do not need to go directly to the properties to buy a house; instead, you can go to the sales office to understand the situation. This reduces the coupling between you (the buyer) and the two property classes. However, applying the Law of Demeter may lead to a consequence where the system has a large number of intermediary classes, which exist to convey the mutual calling relationships between classes, thus increasing the complexity of the system to some extent. The core concept of the Law of Demeter is: decoupling between classes, weak coupling.

1.6 Interface Segregation Principle

The Interface Segregation Principle is used to appropriately divide roles and interfaces, with two meanings: (1) users should not depend on interfaces they do not need; (2) the dependency relationships between classes should be based on the smallest interfaces. Summarizing these two definitions in one sentence: establish a single interface instead of a large and bloated interface. In simple terms, interfaces should be as fine-grained as possible while ensuring that the methods within the interface are kept to a minimum. When an interface contains too many behaviors, it leads to abnormal dependency relationships with clients, and the goal is to separate interfaces to achieve decoupling. Returning to the Single Responsibility Principle, which requires behavior separation and interface refinement, there seems to be some similarity. However, in reality, the Single Responsibility Principle requires that the responsibilities of classes and interfaces be singular, focusing on responsibilities without requiring that interfaces be minimized. In the Interface Segregation Principle, it is required to use multiple specialized interfaces as much as possible. Specialized interfaces are those provided to multiple modules; if they are provided to several modules, there should be several interfaces rather than a large and bloated interface that all modules can access. However, the design of interfaces has its limits. The finer the granularity of the interface design, the more flexible the system is, but too many interfaces can also complicate the structure and increase maintenance difficulty. Therefore, in practice, how to grasp this relies on the experience and common sense of the developers.

The above introduces six design principles, each considering from different angles how a class should be designed to better decouple and facilitate maintenance and extension. In my personal understanding, I believe the first three are more important, while the latter content is an extension and supplement to the first three. The first three principles discuss how to construct a class, how to extend functionality, and how to establish calling relationships between them, essentially answering how to structure code during development, which is worth careful consideration.

2 UML Class Diagrams

When designing code structure, UML class diagrams are a very useful tool (even though everyone dislikes them). However, it is still necessary to briefly introduce their main content, as this helps us understand the essence of code structure design in various design patterns.

2.1 UML Class Diagram CompositionComposition

A standard class consists of Class Name, Attributes, and Operations three parts. As shown in the figure below, UML class diagrams encapsulate them within a rectangular box, arranged from top to bottom.

C++ Design Patterns

1. Class Name: Located at the top of the rectangular box, starting with a capital letter in bold. It is the identifier of the class (e.g., <span>Student</span>).

2. Attributes: Located below the class name, describing the characteristics or state of the class. The syntax for representing attributes in UML class diagrams is: <span>[Visibility] AttributeName: Type [= DefaultValue]</span>,

3. Operations: Located below the attributes, defining the behavior or methods of the class. The syntax for representing operations in UML class diagrams is: <span>[Visibility] OperationName(ParameterName: ParameterType): ReturnType</span>

The visibility of familiar operations is represented using the following symbols:

Symbol Visibility Access Permission Description Keyword
Private Only allowsthe class defining this member to access it internally. <span>private</span>
+ Public Allowsany other class to access it. <span>public</span>
# Protected Allowsthe class defining this member and its subclasses to access it. <span>protected</span>

2.2 UML Class Diagram RelationshipsRelationships

UML class diagrams use different connecting lines to represent the relationships between classes, including association, dependency, generalization, aggregation, and composition.

Association: Represented by a solid line indicating a connection between one class of objects and another. For example, there is a bidirectional “teaching” association between <span>Teacher</span> and <span>Student</span>.

C++ Design Patterns

Dependency: Represented by a dashed line with an arrow pointing to the class being depended on. Dependency is a temporary, weak relationship. For example, the <span>read</span> method of the <span>Person</span> class takes the <span>Book</span> class as a parameter. <span>Person</span> depends on <span>Book</span>, but this relationship is not permanent.

C++ Design Patterns

Generalization: i.e., inheritance, represented by a solid line with a hollow triangle arrow pointing to the parent class. For example, <span>Dog</span> and <span>Cat</span> are both types of <span>Animal</span>, inheriting from the <span>Animal</span> parent class.

C++ Design Patterns

Aggregation: Represents a part-whole relationship, where both can exist independently, indicated by a solid line with a hollow diamond arrow pointing to the whole. For example, a <span>Department</span> consists of <span>Employee</span>s. However, if the department is dissolved, the employees still exist (they may be transferred to other departments).

C++ Design Patterns

Composition: Also represents a part-whole relationship, but the difference is that in a composition relationship, the part cannot exist independently of the whole. It is indicated by a solid line with a filled diamond arrow pointing to the whole. For example, a <span>House</span> consists of <span>Room</span>s. If the house is destroyed, the rooms cease to exist as well. Lifecycle consistency is the key difference between composition and aggregation.

C++ Design Patterns

UML class diagrams describe the relationships between classes in a graphical manner. Understanding this representation of code structure is very helpful for understanding design patterns.

3 Design Patterns

3.1 Factory Pattern

The Factory Pattern defines an interface for creating objects but lets subclasses decide which class to instantiate. The factory method delays the instantiation of a class to its subclasses.The core is “delay”, separating the code for creating objects from the parent class and placing it in the subclasses, in accordance with the “Dependency Inversion Principle” (depend on abstractions rather than concrete implementations). This reduces the coupling between codes, and when modification of instantiation logic is needed, only the factory class needs to be modified without changing the client code.

C++ Design Patterns

As shown in the figure above, when implementing the factory pattern, an abstract <span>Creator</span> class is typically defined, which contains an abstract <span>FactoryMethod</span>. Each concrete <span>ConcreteCreator</span> is responsible for implementing this factory method, returning a specific <span>ConcreteProduct</span>. The pure virtual function FactoryMethod() declared in the abstract Creator class has a return type of a pointer or reference to the abstract base class Product; since the code in Creator only operates according to the Product interface and does not directly hold or instantiate any specific product, there is a “create-use” dependency relationship between Creator and Product—Creator depends on the definition of Product, but the actual object is instantiated and returned by the subclass ConcreteCreator in FactoryMethod(). This forms a concise inheritance and creation chain of “Creator→ConcreteCreator→ConcreteProduct”.

The factory pattern adheres to the Open-Closed Principle and the Single Responsibility Principle, achieving high decoupling of code, but it also introduces many subclasses, increasing system complexity. It is suitable when a class cannot foresee the class of objects it must create, or when a class wants its subclasses to specify the objects it creates.

C++ Code Implementation

#include <iostream>
#include <memory>

// ---------- Abstract Product ----------
class Product {
public:
    virtual ~Product() = default;
    virtual void Operation() const = 0;
};

// ---------- Concrete Product ----------
class ConcreteProductA :public Product {
public:
    void Operation() const override {
        std::cout << "ConcreteProductA::Operation\n";
    }
};

class ConcreteProductB :public Product {
public:
    void Operation() const override {
        std::cout << "ConcreteProductB::Operation\n";
    }
};

// ---------- Abstract Creator ----------
class Creator {
public:
    virtual ~Creator() = default;

    // Factory method: implemented by subclasses
    virtual std::unique_ptr<Product> FactoryMethod() const = 0;
    void SomeOperation() const {
        auto product = FactoryMethod();   // Create specific product
        product->Operation();             // Use through abstract interface
    }
};

// ---------- Concrete Creator ----------
class ConcreteCreatorA :public Creator {
public:
    std::unique_ptr<Product> FactoryMethod() const override {
        return std::make_unique<ConcreteProductA>();
    }
};

class ConcreteCreatorB :public Creator {
public:
    std::unique_ptr<Product> FactoryMethod() const override {
        return std::make_unique<ConcreteProductB>();
    }
};
int main() {
    ConcreteCreatorA creatorA;
    creatorA.SomeOperation();   // Output: ConcreteProductA::Operation

    ConcreteCreatorB creatorB;
    creatorB.SomeOperation();   // Output: ConcreteProductB::Operation

    return 0;
}

3.2 Singleton Pattern

The Singleton Pattern ensures that a class has only one instance and provides a global access point to it. Its core lies in controlling the instantiation process: by privatizing the constructor, it prohibits external creation of objects; it creates and retains this unique instance internally; and provides this instance to the outside through a public static method. This is very useful when a global state or resource management is needed.

C++ Design Patterns

The main implementations of the Singleton Pattern include: lazy initialization and eager initialization.

  1. Lazy Initialization: The instance is created only when the <span>getInstance()</span> method is called for the first time. Its advantage is that it avoids unnecessary resource consumption, creating the instance only when needed. However, it requires handling multithreading safety issues, making the implementation slightly complex.
  2. Eager Initialization: The instance is created at program startup (when static variables are initialized). The advantage is that it is thread-safe without additional synchronization overhead. However, if the instance is never used, it can lead to resource waste; it cannot create instances based on parameters at runtime (if construction requires parameters).

The Singleton Pattern is suitable for objects that are frequently created and destroyed, have high performance overhead during creation, and need to be used frequently (such as thread pools, connection pools), as well as utility class objects and those that frequently access databases or files (such as loggers, configuration managers). The Singleton Pattern, while strictly controlling access and saving resources, also violates the Single Responsibility Principle, hiding dependencies between classes and increasing code coupling. In implementation, for multithreading scenarios, the lazy initialization method requires using double-checked locking and local static variables to ensure thread safety.

C++ Code Implementation (Lazy Initialization – Double-Checked Locking)

#include <iostream>
#include <mutex>

class Singleton {
private:
    static Singleton* instance;
    static std::mutex mtx; 

    // Private constructor to prevent external instantiation
    Singleton(const std::string& value) : value_(value) {}
    ~Singleton() {}
    // Prevent copy and assignment
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    std::string value_;

public:
    // Global access point to get the unique instance
    static Singleton* getInstance(const std::string& value);
    void someBusinessLogic() {
        std::cout << "Value: " << value_ << std::endl;
    }
    std::string getValue() const {
        return value_;
    }
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;

Singleton* Singleton::getInstance(const std::string& value) {
    // Double-checked locking
    if (instance == nullptr) { 
        std::lock_guard<std::mutex> lock(mtx); 
        if (instance == nullptr) { 
            instance = new Singleton(value);
        }
    }
    return instance;
}
int main() {
    Singleton* s1 = Singleton::getInstance("Instance #1");
    Singleton* s2 = Singleton::getInstance("Instance #2");

    std::cout << "s1 value: " << s1->getValue() << "\n";
    std::cout << "s2 value: " << s2->getValue() << "\n";
    std::cout << "Are they the same instance? " << (s1 == s2 ? "Yes" : "No") << std::endl;

    s1->someBusinessLogic();
    return 0;
}
/* Output:
s1 value: Instance #1
s2 value: Instance #1
Are they the same instance? Yes
*/

Most Common Version

class MeyerSingleton {
private:
    MeyerSingleton() = default;
    ~MeyerSingleton() = default;
public:
    MeyerSingleton(const MeyerSingleton&) = delete;
    MeyerSingleton& operator=(const MeyerSingleton&) = delete;

    static MeyerSingleton& getInstance() {
        static MeyerSingleton instance; // C++11 guarantees thread-safe initialization of local static variables
        return instance;
    }
};

3.3 Decorator Pattern

The Decorator Pattern allows dynamically adding extra functionality to an object by wrapping it in a decorator class that has the same interface as the original class, enabling the client code to use the new functionality without modification. In terms of adding functionality, the decorator pattern is more flexible than generating subclasses. It extends the functionality of objects through composition rather than inheritance. The decorator and the decorated object implement the same interface, allowing the decorator to transparently replace the decorated object.

The Decorator Pattern typically consists of the following components:

  1. Abstract Component: Defines the object interface, allowing dynamic addition of functionality.
  2. Concrete Component: Implements the abstract component’s interface and is the object being decorated in the decorator pattern.
  3. Decorator: Holds a reference to an abstract component and implements the same interface as the abstract component. The decorator can add new functionality based on the created concrete component object.
  4. Concrete Decorator: Implements the decorator interface and adds new functionality to the concrete component through composition.

C++ Design Patterns

As shown in the figure, the Component defines a unified interface operation(); ConcreteComponent provides the most basic implementation. The Decorator also inherits from Component and holds a Component* pointer (wrappee) through composition, forwarding calls while also inserting additional behavior before and after; ConcreteDecoratorA and ConcreteDecoratorB further inherit from Decorator, adding new states or methods while forwarding operation(), thus forming a “nested decoration chain” of “object wrapping object” that allows dynamic extension of functionality at runtime without modifying the original class.

The decorator pattern uses composition instead of inheritance to layer additional functionality around the original object, achieving dynamic addition and removal of responsibilities at runtime while remaining transparent to the client; its advantages include complete adherence to the Open-Closed Principle, avoiding subclass explosion, and supporting arbitrary combinations of extensions in order and hierarchy. Its disadvantages include the potential for a large number of fine-grained small classes, increasing debugging and understanding costs, and multiple layers of wrapping may introduce slight performance overhead. It is most suitable for scenarios where “dynamic and reversible functionality needs to be added to objects,” such as adding scroll bars/borders to GUI components, logging, caching, permission validation, compression, and encryption as cross-cutting concerns.

C++ Code Implementation (Data Stream Processing Example)

#include <iostream>
#include <memory>

// ---------- Component ----------
class Component {
public:
    virtual ~Component() = default;
    virtual int operation() const = 0;
};

// ---------- Concrete Component ----------
class ConcreteComponent :public Component {
public:
    int operation() const override {
        return 1;               // Basic behavior
    }
};

// ---------- Decorator ----------
class Decorator :public Component {
public:
    explicit Decorator(std::unique_ptr<Component> w)
        : wrappee_(std::move(w)) {}

    int operation() const override {
        return wrappee_->operation();   // Default only forwards
    }
protected:
    std::unique_ptr<Component> wrappee_;
};

// ---------- Concrete Decorator A ----------
class ConcreteDecoratorA :public Decorator {
public:
    explicit ConcreteDecoratorA(std::unique_ptr<Component> w)
        : Decorator(std::move(w)) {}

    int operation() const override {
        addedState_ = "✓";            // Add new state
        return wrappee_->operation() + 10;
    }
private:
    mutable std::string addedState_;    
};

// ---------- Concrete Decorator B ----------
class ConcreteDecoratorB :public Decorator {
public:
    explicit ConcreteDecoratorB(std::unique_ptr<Component> w)
        : Decorator(std::move(w)) {}

    int operation() const override {
        addedBehavior();                // Add new behavior
        return wrappee_->operation() + 20;
    }
private:
    void addedBehavior() const {
        std::cout << "[ConcreteDecoratorB] extra behavior\n";
    }
};

// ---------- Client ----------
int main() {

    auto simple = std::make_unique<ConcreteComponent>();
    std::cout << "simple: " << simple->operation() << '\n';
    auto decA = std::make_unique<ConcreteDecoratorA>(std::move(simple));
    std::cout << "decA:   " << decA->operation() << '\n';


    auto decB = std::make_unique<ConcreteDecoratorB>(std::move(decA));
    std::cout << "decB:   " << decB->operation() << '\n';
    return 0;
}
/* Output:
simple: 1
decB:   31
*/

3.4 Adapter Pattern

The Adapter Pattern is used to solve the problem of incompatible interfaces by converting an originally incompatible interface into the interface expected by the client through an adapter class, allowing originally non-working classes to work together.

C++ Design Patterns

In the class diagram above, the Client only recognizes the Target interface and calls its request(); the Adapter implements Target, internally aggregating an Adaptee pointer, converting the request() call made by the Client into Adaptee’s specificRequest(), allowing the originally incompatible Adaptee to be seamlessly used by the Client.

The Adapter Pattern encapsulates the conversion interface, allowing originally incompatible classes to work together. Its advantages include decoupling the client from legacy code, reusing existing functionality without modifying the original implementation; its disadvantages include introducing an additional indirect layer, increasing complexity and debugging difficulty. It is suitable for integrating third-party libraries, upgrading old systems, unifying multi-version interfaces, or cross-language calls in scenarios where “existing but interface mismatched” is present.

C++ Code Implementation

#include <iostream>
#include <memory>

// ---------- Target Interface ----------
class Target {
public:
    virtual ~Target() = default;
    virtual void request() const = 0;
};

// ---------- Existing Adaptee ----------
class Adaptee {
public:
    void specificRequest() const {
        std::cout << "Adaptee::specificRequest()\n";
    }
};

// ---------- Adapter ----------
class Adapter :public Target {
public:
    explicit Adapter(std::unique_ptr<Adaptee> adaptee)
        : adaptee_(std::move(adaptee)) {}

    void request() const override {
        adaptee_->specificRequest();   // Forward the call to Adaptee
    }
private:
    std::unique_ptr<Adaptee> adaptee_;
};

// ---------- Client ----------
void clientCode(const Target& target) {
    target.request();   // Only recognizes Target interface
}

int main() {
    std::unique_ptr<Adaptee> adaptee = std::make_unique<Adaptee>();
    std::unique_ptr<Target> adapter = std::make_unique<Adapter>(std::move(adaptee));

    clientCode(*adapter);   
    return 0;
}
/* Output:
Adaptee::specificRequest()
*/

3.5 Strategy Pattern

The Strategy Pattern defines a family of algorithms, encapsulates them, and makes them interchangeable. The Strategy Pattern allows the algorithm to vary independently from the clients that use it. By composition, it separates the use of an algorithm from its implementation.

The Strategy Pattern mainly consists of the following structures:

  1. Strategy Interface, which declares the common operations for all concrete strategies.
  2. Concrete Strategy Classes that implement the Strategy Interface, each encapsulating a specific algorithm or behavior.
  3. Context Class, which contains a reference to a Strategy object and provides a setter to switch strategies.

C++ Design Patterns

As shown in the class diagram above, the Context aggregates a Strategy pointer, switching different implementations at runtime through setStrategy; the Strategy interface declares algorithm(), while ConcreteStrategyA/B/C each inherit from Strategy and provide differentiated algorithms, allowing the Context to call strategy->algorithm() in execute() to obtain the behavior corresponding to the current strategy object, achieving complete decoupling between the algorithm and the user.

The Strategy Pattern encapsulates varying algorithms into a family of interchangeable strategy classes, allowing the Context to dynamically switch implementations at runtime. Its advantages include completely eliminating conditional branches, adhering to the Open-Closed Principle, and facilitating unit testing; its disadvantages include class bloat when the number of strategies is large, and the client must understand the differences between different strategies. It is suitable for scenarios where a family of algorithms frequently changes or needs to be freely switched at runtime, such as payment gateways, sorting/compression algorithms, membership discounts, logging levels, and AI decision engines.

C++ Code Implementation (Data Stream Processing Example)

// strategy.cpp
#include <iostream>
#include <memory>

// ---------- Strategy ----------
class Strategy {
public:
    virtual ~Strategy() = default;
    virtual int algorithm() const = 0;
};

// ---------- Concrete Strategy ----------
class ConcreteStrategyA :public Strategy {
public:
    int algorithm() const override {
        return 10;
    }
};

class ConcreteStrategyB :public Strategy {
public:
    int algorithm() const override {
        return 20;
    }
};

class ConcreteStrategyC :public Strategy {
public:
    int algorithm() const override {
        return 30;
    }
};

// ---------- Context ----------
class Context {
public:
    // Inject strategy at runtime
    void setStrategy(std::unique_ptr<Strategy> s) {
        strategy_ = std::move(s);
    }
    // Delegate to current strategy
    int execute() const {
        return strategy_ ? strategy_->algorithm() : 0;
    }
private:
    std::unique_ptr<Strategy> strategy_;
};

// ---------- Client ----------
int main() {
    Context ctx;

    ctx.setStrategy(std::make_unique<ConcreteStrategyA>());
    std::cout << "Strategy A: " << ctx.execute() << '\n';

    ctx.setStrategy(std::make_unique<ConcreteStrategyB>());
    std::cout << "Strategy B: " << ctx.execute() << '\n';

    ctx.setStrategy(std::make_unique<ConcreteStrategyC>());
    std::cout << "Strategy C: " << ctx.execute() << '\n';

    return 0;
}
/* Output:
Strategy A: 10
Strategy B: 20
Strategy C: 30
*/

3.6 Observer Pattern

The Observer Pattern defines a one-to-many dependency between objects, so that when the state of one object (Subject) changes, all objects that depend on it (Observers) are notified and updated automatically. It is also known as the Publish-Subscribe Pattern. This pattern can be used to implement event-driven systems.

C++ Design Patterns

In the class diagram, the Subject interface defines methods for maintaining/notifying observers such as attach, detach, and notify; ConcreteSubject inherits this interface, maintaining a list of Observers and its own state, and when the state changes, it traverses the list and calls update(Subject) for each registered observer through the base class Observer pointer. The Observer interface declares update, while ConcreteObserverA/B inherit from Observer and implement update, allowing ConcreteSubject to broadcast state updates to all registered concrete observers through a “one-to-many” aggregation relationship (solid line with diamond).

The Observer Pattern transforms the static coding of one-to-many dependencies between objects into dynamic runtime registration, allowing the Subject to broadcast events without knowing the details of the observers, thus reducing coupling and facilitating independent extension and reuse; however, when the number of observers is large or the notification chain is complex, it may lead to performance jitter, debugging difficulties, and potential risks of circular references and memory leaks. It is most suitable for scenarios where “the state change of one object needs to synchronize multiple other objects,” such as GUI event systems, Model-View updates in MVC, message buses, cache invalidation broadcasts, and distributed configuration center pushes.

C++ Code Implementation

#include <iostream>
#include <vector>
#include <algorithm>
#include <memory>

// ---------- Observer ----------
class Subject;                       // Forward declaration
class Observer {
public:
    virtual ~Observer() = default;
    virtual void update(const Subject&) = 0;
};

// ---------- Subject ----------
class Subject {
public:
    virtual ~Subject() = default;

    void attach(std::shared_ptr<Observer> o) {
        observers_.push_back(o);
    }
    void detach(std::shared_ptr<Observer> o) {
        observers_.erase(
            std::remove(observers_.begin(), observers_.end(), o),
            observers_.end());
    }
    void notify() {
        for (const auto& o : observers_) o->update(*this);
    }
protected:
    std::vector<std::shared_ptr<Observer>> observers_;
};

// ---------- Concrete Subject ----------
class ConcreteSubject :public Subject {
public:
    int getState() const { return state_; }
    void setState(int s) {
        state_ = s;
        notify();               // State change → notify observers
    }
private:
    int state_ = 0;
};

// ---------- Concrete Observer ----------
class ConcreteObserverA :public Observer {
public:
    explicit ConcreteObserverA(std::string name) : name_(std::move(name)) {}
    void update(const Subject& s) override {
        const auto& cs = static_cast<const ConcreteSubject&>(s);
        std::cout << name_ << " received update, new state = "
                  << cs.getState() << '\n';
    }
private:
    std::string name_;
};

class ConcreteObserverB :public Observer {
public:
    explicit ConcreteObserverB(std::string name) : name_(std::move(name)) {}
    void update(const Subject& s) override {
        const auto& cs = static_cast<const ConcreteSubject&>(s);
        std::cout << name_ << " ***got*** new state = "
                  << cs.getState() << '\n';
    }
private:
    std::string name_;
};

// ---------- Client ----------
int main() {
    ConcreteSubject subject;

    auto obs1 = std::make_shared<ConcreteObserverA>("ObserverA-1");
    auto obs2 = std::make_shared<ConcreteObserverB>("ObserverB-1");
    auto obs3 = std::make_shared<ConcreteObserverA>("ObserverA-2");

    subject.attach(obs1);
    subject.attach(obs2);
    subject.attach(obs3);

    subject.setState(42);   // All observers receive notification
    subject.detach(obs2);
    subject.setState(100);  // Only obs1 & obs3 receive notification

    return 0;
}
/* Output:
ObserverA-1 received update, new state = 42
ObserverB-1 ***got*** new state = 42
ObserverA-2 received update, new state = 42
ObserverA-1 received update, new state = 100
ObserverA-2 received update, new state = 100
*/

3.7 Template Pattern

The Template Pattern defines the skeleton of an algorithm, deferring some steps of its implementation to subclasses. This allows subclasses to redefine certain steps of the algorithm without changing its structure.

C++ Design Patterns

In the Template Pattern, the AbstractClass declares an immutable template method templateMethod() that calls steps step1, step2, and an optional step3 in a fixed order; where step1 and step2 are declared as pure virtual functions, forcing ConcreteClassA and ConcreteClassB to inherit and implement their respective differentiated logic. At runtime, the client instantiates any specific subclass through a base class pointer and calls templateMethod(), obtaining polymorphic behavior under a unified process skeleton.

The Template Pattern fixes the skeleton of the algorithm in the base class, leaving only the variable steps for subclasses to implement. Its advantages include avoiding duplication of common processes, facilitating reuse and extension, and ensuring that subclasses cannot change the overall execution order, thus guaranteeing algorithm consistency; its disadvantages include difficulty for the parent class to cope with frequently changing processes, complexity in inheritance hierarchy as subclasses increase, and high coupling with the parent class. It is suitable for scenarios where the algorithm framework is stable but certain steps differ significantly, such as various stages of a compiler, banking transaction processes, game AI decision trees, report generation, and continuous integration pipelines.

C++ Code Implementation

#include <iostream>

// ---------- Abstract Class ----------
class AbstractClass {
public:
    // Template method: fixed algorithm skeleton
    void templateMethod() const {
        step1();
        step2();
        step3();        // Optional default implementation
    }

    // Steps that must be customized by subclasses
    virtual void step1() const = 0;
    virtual void step2() const = 0;

    // Optional step: default implementation provided by parent class
    virtual void step3() const {
        std::cout << "[Abstract] default step3\n";
    }

    virtual ~AbstractClass() = default;
};

// ---------- Concrete Class A ----------
class ConcreteClassA :public AbstractClass {
public:
    void step1() const override {
        std::cout << "[A] step1\n";
    }
    void step2() const override {
        std::cout << "[A] step2\n";
    }
};

// ---------- Concrete Class B ----------
class ConcreteClassB :public AbstractClass {
public:
    void step1() const override {
        std::cout << "[B] step1\n";
    }
    void step2() const override {
        std::cout << "[B] step2\n";
    }

    // Override optional step
    void step3() const override {
        std::cout << "[B] custom step3\n";
    }
};

// ---------- Client ----------
int main() {
    ConcreteClassA a;
    ConcreteClassB b;

    std::cout << "=== Run A ===\n";
    a.templateMethod();

    std::cout << "\n=== Run B ===\n";
    b.templateMethod();

    return 0;
}
/* Output:
=== Run A ===
[A] step1
[A] step2
[Abstract] default step3

=== Run B ===
[B] step1
[B] step2
[B] custom step3
*/

3.8 Proxy Pattern

The Proxy Pattern is used to control access to an object. The proxy class can add some additional logic when accessing the object, such as permission checks, lazy loading, etc.

C++ Design Patterns

In the class diagram above, the Subject defines a unified interface request(); RealSubject implements the actual business logic; Proxy also inherits from Subject and internally aggregates a RealSubject pointer, which can add control logic such as permissions, caching, or lazy loading in its own request() before deciding whether or when to forward the call to RealSubject, allowing the Client to transparently use the proxied object through the Subject interface.

The Proxy Pattern inserts a proxy object between the client and the real object to control access, lazy loading, permission validation, caching, or logging, among other cross-cutting logic. Its advantages include transparency to the client, dynamic replacement of proxies, reduced coupling, and enhanced security and performance; its disadvantages include increased indirect layers, class bloat, and potentially slight performance loss in extreme scenarios. It is suitable for scenarios such as remote calls (RPC), lazy loading of images/videos, permission gateways, caching proxies, firewalls, logging monitoring, and AOP aspects.

C++ Code Implementation

// proxy.cpp
#include <iostream>
#include <memory>

// ---------- Abstract Subject ----------
class Subject {
public:
    virtual ~Subject() = default;
    virtual void request() const = 0;
};

// ---------- Real Subject ----------
class RealSubject :public Subject {
public:
    void request() const override {
        std::cout << "RealSubject::request() - heavy work\n";
    }
};

// ---------- Proxy ----------
class Proxy :public Subject {
public:
    explicit Proxy(std::unique_ptr<RealSubject> real)
        : real_(std::move(real)) {}

    void request() const override {
        // Additional control logic: lazy loading, permissions, caching, etc.
        std::cout << "[Proxy] access check & lazy load\n";
        if (!real_) real_ = std::make_unique<RealSubject>();
        real_->request();
    }
private:
    mutable std::unique_ptr<RealSubject> real_;
};

// ---------- Client ----------
void clientCode(const Subject& subject) {
    subject.request();
}

int main() {
    Proxy proxy(nullptr);        // Proxy creates RealSubject on demand
    clientCode(proxy);
    return 0;
}
/* Output:
[Proxy] access check & lazy load
RealSubject::request() - heavy work
*/

Conclusion

This article systematically organizes the UML class diagram symbols and nine frequently used design patterns (Factory, Singleton, Decorator, Adapter, Strategy, Observer, Template, Proxy) based on six design principles, demonstrating the core mechanisms, applicable scenarios, and trade-offs of each pattern with complete C++ source code. Based on the complete knowledge chain of “principles → diagrams → code”: it introduces how to design the structure of code in various situations, achieving high cohesion, low coupling, and easy extensibility in C++ architecture with minimal modification costs.

Click the blue textC++ Design PatternsFollow usC++ Design PatternsNexLab Confronting the Future Infinite Progress

Leave a Comment