Design Patterns in C++
1. Introduction
1.1 What are design patterns?
Design patterns represent solutions to common problems in software design. They can be thought of as reusable templates that developers can apply in various situations, regardless of the specific requirements of the task at hand. In other words, they encapsulate best practices learned over time by experienced software developers. By understanding and utilizing design patterns, developers can write more efficient, scalable, and maintainable code.
1.2 Why are they important?
-
Efficiency: Design patterns provide tested and proven solutions, saving developers from "reinventing the wheel" every time they face a common problem.
-
Maintainability: They introduce consistency into code, making it easier to read and refactor.
-
Scalability: By adhering to patterns, the codebase can more easily accommodate future changes or expansion.
-
Communication: Patterns create a common language among developers. By simply referring to a pattern by name, a developer can convey a lot of information about their design decision without going into minute details.
2. Basic Terms
Here are some fundamental terms that are commonly associated with design patterns in the context of C++:
-
Handle: A reference to another object. In C++, a handle might be implemented as a pointer or a reference that provides indirect access to an object, allowing for operations like sharing and management of the object's lifecycle.
-
Interface: In C++, an interface can be represented by an abstract class, which defines a contract that derived classes must adhere to. It ensures consistency in the way certain types of objects interact with the rest of the system.
-
Concrete Class: This is a class that implements an interface (or abstract class) by providing definitions for all its pure virtual functions. Instances of concrete classes can be created and used directly.
-
Abstract Class: A class that cannot be instantiated and usually contains one or more pure virtual functions. It's often used as a base for other classes.
-
Singleton: A design pattern that ensures a class has only one instance and provides a way to access it globally. In C++, it's often implemented with a static instance inside the class.
-
Encapsulation: The bundling of data (attributes) and methods (functions) that operate on the data into a single unit called a class. It restricts direct access to an object's state and ensures data integrity.
-
Polymorphism: The ability of different classes to be treated as instances of the same class through inheritance. In C++, this is commonly achieved using virtual functions.
-
Aggregation & Composition: Both represent "has-a" relationships between classes. Aggregation implies a relationship where the child can exist independently of the parent, while composition suggests a strong life-cycle dependency between the parent and the child.
3. Types of Design Patterns
3.1 Creational Patterns
Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. The basic approach to object creation might otherwise lead to added complexity in a system. The creational patterns encapsulate this complexity. Some of the key patterns under this category are:
Pattern | Description | Use Cases | Pros | Cons | Additional Info |
---|---|---|---|---|---|
Singleton | Ensure a class has only one instance and provide a global point of access. | Database connections, Logger, Configurations. | - Avoids duplicate resource usage. - Lazily loaded, i.e., loaded on demand. |
- Can be problematic in multi-threading. - May hide bad design (global state). |
Watch out for thread-safety issues. |
Factory Method | Define an interface for creating an object, but let subclasses decide which class to instantiate. | GUI libraries, Payment gateway integration. | - Promotes loose coupling. - Follows the open/closed principle. |
- Can lead to proliferation of classes. | Suitable when there's a generic process with specific steps. |
Abstract Factory | Provide an interface for creating families of related or dependent objects without specifying their concrete classes. | Cross-platform UI elements, creating product sets with multiple variations. | - Isolates concrete classes. - Enhances modularity. |
- Can be complex due to a lot of new subclasses. | Ideal when systems need to be configured with multiple families of products. |
Builder | Separate the construction of a complex object from its representation. | Complex object creation, Parsing complex representations to objects. | - Allows for varying a product's internal representation. - Isolates code for construction and representation. |
- Can lead to a large number of small classes. | Good for objects that require a lot of steps or configurations during creation. |
Prototype | Specify the kind of objects to create using a prototypical instance and create new objects by copying the prototype. | Cloneable objects, undo/redo functionality in apps. | - Reduces the need for subclasses. - Faster instantiation compared to deep copying. |
- Deep vs. shallow copy issues. - Overhead of cloning. |
Ensure the objects you're cloning support the cloning operation properly. |
3.2 Structural Patterns
Structural patterns concern class and object composition. They provide a way to assemble objects and classes into larger structures, while ensuring that the structures remain independent and efficient. Key patterns include:
Pattern | Description | Use Cases | Pros | Cons | Additional Info |
---|---|---|---|---|---|
Adapter | Match interfaces of different classes to allow them to work together. | Integration of old systems with new systems, Adapting varying interfaces. | - Increased compatibility. - Reusability of classes with different interfaces. |
- Overhead due to the introduction of the adapter. | Acts like a bridge between two incompatible interfaces. |
Bridge | Separate an object’s interface from its implementation. | Drawing tools, Remote controllers for devices. | - Decouples abstraction from implementation. - Enhances extensibility. |
- Increased complexity. | Ideal when both abstraction and its implementation should evolve independently. |
Composite | Build a hierarchy of objects to treat individual and composite objects uniformly. | Graphics systems, File & directory systems. | - Simplifies the client code. - Makes it easier to add new types of components. |
- Can make design overly general. | Useful for tree-like structures. |
Decorator | Add responsibilities to an object dynamically. | GUI components, Middleware in web applications. | - More flexible than static inheritance. - Enhances extensibility. |
- Can result in many small objects. - Might complicate the code. |
Can be stacked in any order. |
Facade | Provide a unified interface to a set of interfaces in a subsystem. | Simplifying complex libraries, Server and database communication. | - Simplifies the interface. - Promotes weak coupling. |
- Can become a monolithic structure if not careful. | Doesn't prohibit access to the subsystem but makes it easier. |
Flyweight | Use sharing to fit more objects in memory by sharing common parts. | Text processors, Game character rendering. | - Reduces the memory footprint. - Faster performance due to fewer instantiations. |
- Complexity can increase. - Outside data might be needed if intrinsic state is removed. |
Externalize object state to reduce the number of unique object instances. |
Proxy | Provide a surrogate or placeholder for another object to control access to it. | Access control, Lazy initialization, Remote object access. | - Can control object creation and access. - Memory saving with lazy initialization. |
- Overhead of proxy object. | Can be used for logging, access control, and lazy loading. |
3.3 Behavioral Patterns
Behavioral patterns deal with the communication between objects, how objects operate, and the responsibility they have. Some well-known behavioral patterns are:
Pattern | Description | Use Cases | Pros | Cons | Additional Info |
---|---|---|---|---|---|
Chain of Responsibility | Decouple sender from receiver by letting more than one object handle a request. | Event handling systems, Logging frameworks. | - Enhanced decoupling. - Dynamic runtime chain configuration. |
- Handling might not be guaranteed. - Performance concerns in long chains. |
Chain order matters. Be careful of unintentional consequences. |
Command | Encapsulate a request as an object. | UI buttons and menu items, Macro recording. | - Decouples invoker from receiver. - Supports undo/redo operations. |
- Might lead to numerous command classes. | Can support logging and transactional operations. |
Interpreter | Include a language in an application to evaluate sentences. | SQL parsers, Configuration systems. | - Easily extend to support new grammar rules. | - Limited for complex grammars. - Performance issues. |
Use for simple grammar or combine with other parsers. |
Iterator | Access the elements of a collection without exposing its underlying representation. | Navigating collections, Database traversal. | - Decouples algorithms from containers. - Multiple traversals simultaneously. |
- Might be seen as overkill for simple collections. | Ensure your iterators are fail-fast to prevent modifications during traversal. |
Mediator | Define simplified communication between classes. | Chat rooms, Air traffic control systems. | - Reduces subclassing. - Decouples many-to-many relationship. |
- Mediator can become a monolith. | Centralizes external communications. |
Memento | Capture and restore an object's internal state. | Undo/redo in software, Snapshots for recovery. | - Doesn't violate encapsulation. - Simplifies the originating object. |
- Can be resource-intensive. | Store only necessary state to save resources. |
Observer | Notify dependent objects of state changes. | Event handling systems, Data binding. | - Promotes loose coupling. - Supports broadcast communication. |
- Unexpected updates. - Might cause performance issues. |
Consider using modern solutions like event systems or reactive streams. |
State | Alter an object’s behavior when its state changes. | Tool modes in graphic editors, Connection state handling. | - Encapsulates state-specific behavior. - Avoids large monolithic conditional statements. |
- Can increase the number of classes. | Can be combined with the Singleton pattern. |
Strategy | Encapsulate an algorithm inside a class. | Payment gateway integrations, Compression algorithms. | - Enables switching algorithms at runtime. - Promotes open/closed principle. |
- Client must be aware of possible strategies. | Can use lambdas in languages that support them to reduce boilerplate. |
Template Method | Define the structure of an algorithm. | Data processing pipelines, Document parsers. | - Code reusability. - Centralizes control logic. |
- Can be hard to modify steps in the sequence. | Subclasses can override steps without changing the overall algorithm's structure. |
Visitor | Add operations to classes without modifying them. | Rendering engines, Syntax tree processing in compilers. | - Separates algorithm from objects. - Adds operations without altering the structure. |
- Might break encapsulation. - Adding new elements can be difficult. |
Make sure elements are stable if using this pattern. |
4. Creational Patterns
Singleton
Description: Ensures that a class has only one instance and provides a single point of access to that instance.
C++ Example:
class Singleton {
private:
static Singleton* instance;
Singleton() {} // Private constructor
public:
// Public method to get the instance of the class
static Singleton* getInstance() {
if (!instance)
instance = new Singleton;
return instance;
}
};
Singleton* Singleton::instance = nullptr;
Factory Method
Description: Defines an interface for creating an object, leaving the responsibility of its instantiation to its subclasses.
C++ Example:
class Product {
public:
virtual void show() = 0;
};
class ConcreteProductA : public Product {
public:
void show() override {
std::cout << "Product A" << std::endl;
}
};
class ConcreteProductB : public Product {
public:
void show() override {
std::cout << "Product B" << std::endl;
}
};
class Creator {
public:
virtual Product* createProduct() = 0;
};
class ConcreteCreatorA : public Creator {
public:
Product* createProduct() override {
return new ConcreteProductA();
}
};
class ConcreteCreatorB : public Creator {
public:
Product* createProduct() override {
return new ConcreteProductB();
}
};
Abstract Factory
Description: Provides an interface for creating families of related or dependent objects without specifying their concrete classes.
C++ Example:
class Button {
public:
virtual void paint() = 0;
};
class MacOSButton : public Button {
public:
void paint() override {
std::cout << "Mac OS Button" << std::endl;
}
};
class WindowsButton : public Button {
public:
void paint() override {
std::cout << "Windows Button" << std::endl;
}
};
class GUIFactory {
public:
virtual Button* createButton() = 0;
};
class MacOSFactory : public GUIFactory {
public:
Button* createButton() override {
return new MacOSButton();
}
};
class WindowsFactory : public GUIFactory {
public:
Button* createButton() override {
return new WindowsButton();
}
};
Builder
Description: Separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
C++ Example:
class Product {
std::string part_;
public:
void add(const std::string& part) {
part_ = part;
}
void show() {
std::cout << "Product built as: " << part_ << std::endl;
}
};
class Builder {
public:
virtual void buildPart() = 0;
virtual Product* getResult() = 0;
};
class ConcreteBuilder : public Builder {
Product* product = new Product();
public:
void buildPart() override {
product->add("Concrete Part");
}
Product* getResult() override {
return product;
}
};
class Director {
Builder* builder;
public:
Director(Builder* b) : builder(b) {}
void construct() {
builder->buildPart();
}
};
Prototype
Description: Specify the kind of objects to create using a prototypical instance, and create new objects by copying this prototype.
C++ Example:
class Prototype {
public:
virtual Prototype* clone() = 0;
};
class ConcretePrototype : public Prototype {
std::string data_;
public:
ConcretePrototype(const std::string& data) : data_(data) {}
Prototype* clone() override {
return new ConcretePrototype(data_);
}
void show() {
std::cout << "Data: " << data_ << std::endl;
}
};
5. Structural Patterns
Adapter
Description: Allows two incompatible interfaces to work together by converting the interface of one class into another.
C++ Example:
class OldSystem {
public:
void oldRequest() {
std::cout << "Old system request." << std::endl;
}
};
class NewSystem {
public:
virtual void request() = 0;
};
class Adapter : public NewSystem {
OldSystem oldSystem;
public:
void request() override {
oldSystem.oldRequest();
}
};
Bridge
Description: Decouples an abstraction from its implementation, allowing both to vary independently.
C++ Example:
class Implementation {
public:
virtual void operationImpl() = 0;
};
class ConcreteImplementationA : public Implementation {
public:
void operationImpl() override {
std::cout << "Implementation A" << std::endl;
}
};
class Abstraction {
protected:
Implementation* impl;
public:
Abstraction(Implementation* i) : impl(i) {}
virtual void operation() {
impl->operationImpl();
}
};
class RefinedAbstraction : public Abstraction {
public:
RefinedAbstraction(Implementation* i) : Abstraction(i) {}
void operation() override {
std::cout << "Refined ";
impl->operationImpl();
}
};
Composite
Description: Composes objects into tree structures to represent whole-part hierarchies.
C++ Example:
class Component {
public:
virtual void operation() = 0;
};
class Leaf : public Component {
public:
void operation() override {
std::cout << "Leaf operation." << std::endl;
}
};
class Composite : public Component {
std::vector<Component*> children;
public:
void add(Component* component) {
children.push_back(component);
}
void operation() override {
for (auto& child : children) {
child->operation();
}
}
};
Decorator
Description: Dynamically adds responsibilities to an object without affecting other objects.
C++ Example:
class Component {
public:
virtual void operation() = 0;
};
class ConcreteComponent : public Component {
public:
void operation() override {
std::cout << "Concrete operation." << std::endl;
}
};
class Decorator : public Component {
protected:
Component* component;
public:
Decorator(Component* c) : component(c) {}
void operation() override {
component->operation();
}
};
class ConcreteDecorator : public Decorator {
public:
ConcreteDecorator(Component* c) : Decorator(c) {}
void operation() override {
Decorator::operation();
std::cout << "Decorated operation." << std::endl;
}
};
Facade
Description: Provides a simplified interface to a group of subsystems.
C++ Example:
class SubsystemA {
public:
void operationA() {
std::cout << "Subsystem A operation." << std::endl;
}
};
class SubsystemB {
public:
void operationB() {
std::cout << "Subsystem B operation." << std::endl;
}
};
class Facade {
SubsystemA a;
SubsystemB b;
public:
void operation() {
a.operationA();
b.operationB();
}
};
Flyweight
Description: Reduces the cost of creating a large number of similar objects by sharing them.
C++ Example:
class Flyweight {
public:
virtual void operation() = 0;
};
class ConcreteFlyweight : public Flyweight {
public:
void operation() override {
std::cout << "Shared operation." << std::endl;
}
};
class FlyweightFactory {
std::unordered_map<std::string, Flyweight*> pool;
public:
Flyweight* getFlyweight(const std::string& key) {
if (pool.find(key) == pool.end()) {
pool[key] = new ConcreteFlyweight();
}
return pool[key];
}
};
Proxy
Description: Provides a placeholder for another object to control access to it.
C++ Example:
class RealObject {
public:
void operation() {
std::cout << "Real object operation." << std::endl;
}
};
class Proxy {
RealObject realObject;
public:
void operation() {
std::cout << "Proxy operation." << std::endl;
realObject.operation();
}
};
Alright! Let's delve into each of these behavioral patterns, offering succinct explanations followed by C++ examples:
6. Behavioral Patterns
Chain of Responsibility
Description: Allows an object to pass the request along a chain of potential handlers until an object handles it or the end of the chain is reached.
C++ Example:
class Handler {
protected:
Handler* successor = nullptr;
public:
void setSuccessor(Handler* s) {
successor = s;
}
virtual void handleRequest(int value) {
if (successor)
successor->handleRequest(value);
}
};
class ConcreteHandlerA : public Handler {
public:
void handleRequest(int value) override {
if (value == 1)
std::cout << "HandlerA handling request." << std::endl;
else if (successor)
successor->handleRequest(value);
}
};
Command
Description: Turns a request into a standalone object that contains information about the request.
C++ Example:
class Command {
public:
virtual void execute() = 0;
};
class ConcreteCommand : public Command {
public:
void execute() override {
std::cout << "Execute command." << std::endl;
}
};
class Invoker {
Command* command;
public:
void setCommand(Command* cmd) {
command = cmd;
}
void invoke() {
command->execute();
}
};
Interpreter
Description: Provides a way to evaluate language grammar or expressions for particular languages.
C++ Example: (This is a bit complex for a concise example, so here's a very simplified version)
class Expression {
public:
virtual bool interpret(const std::string& context) = 0;
};
class TerminalExpression : public Expression {
std::string data;
public:
TerminalExpression(const std::string& d) : data(d) {}
bool interpret(const std::string& context) override {
return context.find(data) != std::string::npos;
}
};
Iterator
Description: Provides a way to access the elements of an aggregate object without exposing its underlying structure.
C++ Example:
template <typename T>
class Iterator {
public:
virtual bool hasNext() = 0;
virtual T next() = 0;
};
template <typename T>
class ConcreteIterator : public Iterator<T> {
std::vector<T> items;
int index = 0;
public:
ConcreteIterator(const std::vector<T>& v) : items(v) {}
bool hasNext() override {
return index < items.size();
}
T next() override {
return items[index++];
}
};
Mediator
Description: Provides a centralized communication medium between different classes.
C++ Example:
class Mediator;
class Colleague {
protected:
Mediator* mediator;
public:
virtual void send(const std::string& message) = 0;
};
class Mediator {
public:
virtual void sendMessage(const std::string& message, Colleague* colleague) = 0;
};
class ConcreteColleague : public Colleague {
public:
void send(const std::string& message) override {
mediator->sendMessage(message, this);
}
void receive(const std::string& message) {
std::cout << "Received: " << message << std::endl;
}
};
Memento
Description: Allows saving and restoring the previous state of an object without revealing the specifics of its implementation.
C++ Example:
class Memento {
std::string state;
public:
Memento(const std::string& s) : state(s) {}
std::string getState() {
return state;
}
};
class Originator {
std::string state;
public:
void setState(const std::string& s) {
state = s;
}
Memento* saveState() {
return new Memento(state);
}
void restoreState(Memento* memento) {
state = memento->getState();
}
};
Observer
Description: Allows an object to publish changes to its state, so other objects can react accordingly.
C++ Example:
class Observer {
public:
virtual void update(const std::string& message) = 0;
};
class ConcreteObserver : public Observer {
public:
void update(const std::string& message) override {
std::cout << "Observer received: " << message << std::endl;
}
};
class Subject {
std::vector<Observer*> observers;
public:
void attach(Observer* o) {
observers.push_back(o);
}
void notify(const std::string& message) {
for (auto& obs : observers) {
obs->update(message);
}
}
};
State
Description: Allows an object to alter its behavior when its internal state changes.
C++ Example:
class State {
public:
virtual void handle() = 0;
};
class ConcreteStateA : public State {
public:
void handle() override {
std::cout << "State A handling." << std::endl;
}
};
class Context {
State* state;
public:
void setState(State* s) {
state = s;
}
void request() {
state->handle();
}
};
Strategy
Description: Allows selecting an implementation of an algorithm's interface at runtime.
C++ Example:
class Strategy {
public:
virtual void algorithm() = 0;
};
class ConcreteStrategyA : public Strategy {
public:
void algorithm() override {
std::cout << "Algorithm A." << std::endl;
}
};
class Context {
Strategy* strategy;
public:
void setStrategy(Strategy* s) {
strategy = s;
}
void execute() {
strategy->algorithm();
}
};
Template Method
Description: Defines the structure of an algorithm, but allows subclasses to implement specific steps.
C++ Example:
class AbstractClass {
public:
void templateMethod() {
baseOperation();
specificOperation();
}
void baseOperation() {
std::cout << "Base operation." << std::endl;
}
virtual void specificOperation() = 0;
};
class ConcreteClass : public AbstractClass {
public:
void specificOperation() override {
std::cout << "Specific operation for ConcreteClass." << std::endl;
}
};
int main() {
ConcreteClass c;
c.templateMethod(); // This will print both "Base operation." and "Specific operation for ConcreteClass."
}
Visitor
Description: Allows you to add further operations to objects without having to modify them.
C++ Example:
class Element;
class ConcreteElementA;
class ConcreteElementB;
class Visitor {
public:
virtual void visitConcreteElementA(ConcreteElementA *element) = 0;
virtual void visitConcreteElementB(ConcreteElementB *element) = 0;
};
class Element {
public:
virtual void accept(Visitor *visitor) = 0;
};
class ConcreteElementA : public Element {
public:
void accept(Visitor *visitor) override {
visitor->visitConcreteElementA(this);
}
void operationA() {
std::cout << "Operation A from ConcreteElementA." << std::endl;
}
};
class ConcreteElementB : public Element {
public:
void accept(Visitor *visitor) override {
visitor->visitConcreteElementB(this);
}
void operationB() {
std::cout << "Operation B from ConcreteElementB." << std::endl;
}
};
class ConcreteVisitor : public Visitor {
public:
void visitConcreteElementA(ConcreteElementA *element) override {
element->operationA();
}
void visitConcreteElementB(ConcreteElementB *element) override {
element->operationB();
}
};
int main() {
ConcreteElementA a;
ConcreteElementB b;
ConcreteVisitor v;
a.accept(&v); // This will print "Operation A from ConcreteElementA."
b.accept(&v); // This will print "Operation B from ConcreteElementB."
}
The above example illustrates the essence of the Visitor pattern. In a real-world scenario, the Visitor pattern allows you to separate algorithms from the objects on which they operate, resulting in a decoupled system. This makes it easier to add new operations to existing object structures without modifying them.
7. C++ Specifics in Design Patterns
C++ Features and Design Patterns
Templates
Description: Templates in C++ allow for type-independent code, which aids in generic and reusable solutions.
How it aids in design patterns:
- Singleton: Ensures that a class has only one instance for each type if we use a templated Singleton.
- Factory: Can produce objects of any type with a templated factory method.
- Strategy: Using templates, we can define strategy algorithms that can work on various data types.
Lambda Functions
Description: Lambdas allow defining anonymous functions inline.
How it aids in design patterns:
- Command: Lambda functions can be used to implement simple command objects without having to define separate classes.
- Strategy: For simple strategies, lambdas can provide an inline approach to define the required algorithm.
Smart Pointers
Description: Smart pointers, like std::shared_ptr
and std::unique_ptr
, handle memory management automatically.
How it aids in design patterns:
- Prototype: When cloning objects, smart pointers can ensure proper memory management.
- Factory: When objects are created and ownership is transferred,
std::unique_ptr
can be employed.
Memory Management and Patterns
In C++, manual memory management can be error-prone. However, design patterns can sometimes hide or abstract away these complexities:
- Composite: Ensuring that child objects are deleted properly can be managed using smart pointers.
- Proxy: When the real object is expensive to create, memory-wise, the proxy can delay its creation or manage its lifecycle.
- Flyweight: Helps in minimizing memory usage by sharing intrinsic data among different objects.
RAII (Resource Acquisition Is Initialization) and Design Patterns
Description: RAII is a principle in C++ where resources are acquired in a constructor and released in a destructor. It ensures that resources are managed properly through object lifecycles.
Benefits in design patterns:
- Singleton: Ensure that any resources used by the singleton instance are appropriately initialized and cleaned up.
- Adapter or Facade: When adapting one interface to another or creating a simplified interface to a complex system, RAII can help manage resources in the adapted or simplified interface.
- Observer: RAII can be useful to ensure that observers are added and removed properly, especially when considering thread safety or preventing dead observers.
- Decorator: If the decorator adds additional resources to an object, RAII can ensure these are managed correctly.