Lesson 00: Understanding Design Patterns
0.1 Why Do We Need Design Patterns?
Why Do We Need Design Patterns?
1.1 The Problem in Real-World C++ Software Development
As C++ systems scale in size and complexity (e.g., embedded firmware, GUI frameworks, game engines, distributed systems), developers face recurring architectural challenges:
- Tight coupling between classes
- Code duplication
- Rigid system structures
- Poor maintainability
- Difficulty extending features
- Hard-to-test modules
These issues are not language problems — they are design problems.
Design patterns provide proven architectural solutions to recurring design challenges.
Read More...
Tight Coupling Between Classes
Tight Coupling Between Classes
Definition
Tight coupling occurs when one class directly depends on the concrete implementation details of another class.
Instead of depending on abstraction, the class depends on:
- Concrete types
- Specific implementations
- Internal behavior
- Construction details
Why Is This a Problem?
- Ripple Effect
Changing one class forces modifications in all dependent classes. - Violates the Dependency Inversion Principle
High-level modules depend on low-level modules. - Prevents Reusability
The class cannot function independently. - Blocks Unit Testing
You cannot isolate dependencies easily.
Example (Bad Design)
class MySQLDatabase {
public:
void connect() {}
void query(const std::string& sql) {}
};
class UserService {
private:
MySQLDatabase db; // Direct dependency
public:
void registerUser() {
db.connect();
db.query("INSERT INTO users...");
}
};
Problem
- UserService cannot work with:
- PostgreSQL
- Mock database
- In-memory DB
- Changing the database implementation breaks UserService
- Hard to unit test without a real DB
Why This Becomes Dangerous in Large Systems
In enterprise systems:
- 10+ layers of dependencies
- Thousands of classes
Tight coupling leads to:
Architecture paralysis
Even small changes require massive regression testing.
Code Duplication
Code Duplication
Definition
Same logic implemented in multiple places.
Why Is This a Problem?
- Maintenance Explosion
If logic changes, you must update multiple locations. - Bug Propagation
A bug duplicated becomes multiple bugs. - Violates DRY Principle
(Don’t Repeat Yourself)
Example
double calculateDiscountVIP(double price) {
return price * 0.8;
}
double calculateDiscountEmployee(double price) {
return price * 0.8;
}
Later, the business changes the discount to 0.75:
If one function is updated but the other is not, → inconsistent system behavior.
Real-World Impact
In legacy C++ systems:
- Copy-paste logic spreads across modules
- Business rules diverge silently
- Refactoring becomes dangerous
Rigid System Structures
Rigid System Structures
Definition
A system is rigid when:
Small changes require modifying many unrelated parts.
Why Is This a Problem?
- High Refactoring Cost
- Risk of Regression
- Slows Feature Development
Example
class Report {
public:
void generatePDF();
void generateHTML();
void generateExcel();
};
Now a new format is required:
- Must modify Report class
- Recompile everything
- Possibly break existing code
This violates:
- Open-Closed Principle
A better design would separate the format strategy.
Consequences in C++
Because C++ requires compilation:
- Small change → full rebuild
- Template-heavy code → long compile times
- Refactoring risk increases
Rigid systems reduce engineering agility.
Poor Maintainability
Poor Maintainability
Definition
Maintainability measures:
How easily can developers understand, modify, and fix the system?
Why Is This a Problem?
Software lifespan:
- 5–20 years (industrial systems)
- Often maintained by different engineers
Poor maintainability leads to:
- Fear of change
- Hidden side effects
- Debugging nightmares
Example
class OrderManager {
public:
void process() {
// 2000 lines of mixed logic:
// DB operations
// UI logic
// Business rules
// Logging
// Validation
}
};
Problems
- No separation of concerns
- Hard to trace logic
- Impossible to test parts independently
Engineering Cost
Maintenance cost often exceeds:
70–80% of the total software lifecycle cost
Poor architecture dramatically increases that percentage.
Difficulty Extending Features
Difficulty Extending Features
Definition
A system is hard to extend when adding functionality requires:
- Editing existing code
- Breaking existing behavior
- Copy-pasting similar logic
Why Is This a Problem?
In real products:
- Requirements change frequently
- Customers demand customization
- Features evolve
If architecture resists change:
- Development slows
- Bugs increase
- Technical debt accumulates
Example (Bad Design)
class PaymentProcessor {
public:
void processPayment(std::string type) {
if (type == "CreditCard") {
// handle credit
}
else if (type == "PayPal") {
// handle paypal
}
else if (type == "Bitcoin") {
// handle bitcoin
}
}
};
Adding new payment type:
- Modify class
- Add more if-else
- Risk of breaking existing logic
This violates:
- Open-Closed Principle
Long-Term Effect
The class grows indefinitely.
Becomes:
“God Class”
Eventually impossible to modify safely.
Hard-to-Test Modules
Hard-to-Test Modules
Definition
A module is hard to test when:
- It depends on external systems
- It creates objects internally
- It has hidden dependencies
- It mixes responsibilities
Why Is This a Problem?
Modern engineering requires:
- Automated testing
- Continuous integration
- Unit testing
Without a testable design:
- Bugs escape to production
- Refactoring becomes risky
- Release cycles slow down
Example
class FileLogger {
public:
void log(const std::string& message) {
std::ofstream file("log.txt");
file << message;
}
};
class OrderService {
private:
FileLogger logger;
public:
void processOrder() {
logger.log("Processing order");
}
};
Problems
- Cannot test OrderService without file I/O
- File system dependency
- No mock injection possible
Testing Cost
Without decoupling:
- Need integration testing instead of unit testing
- Tests become slow
- Debugging becomes complex
Summary Table
| Problem | Root Cause | Long-Term Impact |
|---|---|---|
| Tight Coupling | Concrete dependencies | Ripple effect, low flexibility |
| Code Duplication | Lack of abstraction | Inconsistent behavior |
| Rigid Structure | Poor separation | High modification cost |
| Poor Maintainability | Mixed responsibilities | High lifecycle cost |
| Hard to Extend | Violating OCP | Growing complexity |
| Hard to Test | Hidden dependencies | Low reliability |
Engineering Insight
All these issues share a common root:
Poor management of dependencies and responsibilities.
Design patterns aim to solve:
- How objects are created
- How objects communicate
- How responsibilities are distributed
- How change is isolated
Key Takeaway
In C++ systems:
Poor architecture does not fail immediately. It accumulates technical debt silently. Design patterns exist to prevent:
- Architectural decay
- Codebase entropy
- Long-term maintenance explosion
1.2 Definition of a Design Pattern
A design pattern is:
A reusable, generalized solution to a commonly occurring problem within a given context in software design.
Design patterns are not code templates, and they are not libraries. They are structured design blueprints.
They describe:
- Intent
- Structure
- Roles of participating classes
- Collaboration mechanism
- Consequences
1.3 Historical Context
The concept was popularized in:
- "Design Patterns: Elements of Reusable Object-Oriented Software"
- Authors: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides
- Commonly referred to as the Gang of Four (GoF)
They categorized patterns into:
- Creational Patterns
- Structural Patterns
- Behavioral Patterns
1.4 Why C++ Especially Benefits from Design Patterns
C++ provides:
- Manual memory management
- Multiple inheritance
- Templates (generic programming)
- Polymorphism
- Resource Acquisition Is Initialization (RAII)
- Move semantics
These powerful tools increase flexibility — but also complexity.
Design patterns help:
- Structure object relationships
- Manage lifetime properly
- Avoid memory leaks
- Reduce ownership ambiguity
- Separate interface from implementation
- Encapsulate variability
In modern C++ (C++11–C++23), patterns integrate with:
- Smart pointers (std::unique_ptr, std::shared_ptr)
- Move semantics
- Lambda expressions
- Template metaprogramming
- std::function
0.2. Advantages and Disadvantages of Design Patterns
Advantages and Disadvantages of Design Patterns
2.1 Advantages
1. Proven Solutions
Patterns are battle-tested across decades of production systems.
They reduce trial-and-error design.
2. Improves Code Reusability
Patterns promote:
- Low coupling
- High cohesion
- Encapsulation of change
This enables modular design.
3. Improves Maintainability
By separating concerns:
- Business logic
- Object creation
- Communication
- Responsibilities
Example: Factory Pattern separates object creation from usage.
4. Enhances Communication Between Developers
Patterns create a shared vocabulary:
Instead of explaining architecture in detail:
“We use Observer for event handling.”
The entire structure becomes clear immediately.
5. Supports Open-Closed Principle (OCP)
Most patterns aim to:
Allow extension without modifying existing code.
This is essential for scalable systems.
2.2 Disadvantages
1. Overengineering
Using patterns where a simple design is sufficient increases:
- Complexity
- Indirection
- Maintenance cost
“Not every problem requires a pattern.”
2. Increased Abstraction
Patterns often introduce:
- Interfaces
- Base classes
- Indirection layers
This may reduce readability for beginners.
3. Performance Overhead
Some patterns are introduced:
- Virtual function calls
- Heap allocations
- Additional indirection
In high-performance systems (e.g., embedded systems), this must be evaluated carefully.
4. Misapplication
Incorrect pattern choice may:
- Increase coupling
- Reduce clarity
- Make debugging harder
Pattern usage requires understanding the problem context.
1.3. How Do Design Patterns Work?
How Do Design Patterns Work?
3.1 Core Concept of a Pattern
Every design pattern contains:
- Intent – What problem does it solve?
- Structure – Class relationships
- Participants – Roles of classes
- Collaboration – How objects interact
- Consequences – Trade-offs
3.2 Core Idea Behind Patterns
Patterns typically address one of three design dimensions:
| Category | Focus |
|---|---|
| Creational | Object creation mechanisms |
| Structural | Object composition |
| Behavioral | Object communication |
Ex1: Singleton Pattern (Creational)
Singleton Pattern (Creational)
Intent:
Ensure a class has only one instance and provide global access to it.
Core Concept:
- Private constructor
- Static instance
- Public accessor
C++ Example (Modern C++ Thread-Safe Version)
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // Guaranteed thread-safe since C++11
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
};
Core Mechanism:
- Static local variable initialization
- Deleted copy constructor
- Controlled construction
Ex2: Factory Pattern (Creational)
Factory Pattern (Creational)
Intent:
Encapsulate object creation logic.
Core Concept:
Instead of:
Circle c;
Use:
Shape* shape = ShapeFactory::createShape("Circle");
Advantages:
- Decouples the client from concrete classes
- Centralizes construction logic
- Enables runtime flexibility
Ex3: Observer Pattern (Behavioral)
Observer Pattern (Behavioral)
Intent:
Define a one-to-many dependency between objects. When one object changes state, all dependents are notified.
Core Concept:
- Subject maintains observer list
- Observers register/unregister
- Notification mechanism
3.3 The Real Core: Decoupling
At a deeper level, most patterns aim to:
Reduce coupling while preserving flexibility.
Patterns manage:
- Object ownership
- Responsibility assignment
- Dependency direction
- Variation points
They introduce indirection where necessary.
3.4 Patterns and SOLID Principles
Design patterns operationalize SOLID principles:
- Single Responsibility Principle
- Open-Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Patterns are practical implementations of these principles.
3.5 When to Use a Pattern
Use a pattern when:
- You recognize a recurring design problem
- Code smells appear (rigidity, fragility, immobility)
- You anticipate change in specific dimensions
- Responsibility allocation becomes unclear
Do NOT use a pattern:
- Just to show knowledge
- When simple composition solves the problem
- When requirements are trivial
Summary of Lesson 1
Design patterns:
- Are reusable design solutions
- Improve maintainability and scalability
- Provide shared architectural vocabulary
- Must be applied with contextual judgment
Core idea:
Patterns manage object relationships and responsibilities to reduce coupling and increase flexibility.
0.4 Design Pattern Categories
Design Pattern Categories
Design patterns are traditionally grouped into three primary categories based on what aspect of software design they address:
- Object creation
- Object composition
- Object interaction
Understanding these categories helps developers choose the correct architectural strategy for a given design problem.
1. Creational Patterns
Creational Patterns
Focus
Creational patterns address:
How objects are created and initialized
They abstract the instantiation process to:
- Avoid direct object construction (new)
- Reduce coupling between client code and concrete classes
- Improve flexibility in object creation logic
When to Use
Use creational patterns when:
- Object creation logic is complex
- The exact type of object should be decided at runtime
- You want to decouple client code from specific implementations
- You need controlled instance management
Key Creational Patterns
- Singleton – Ensures a class has only one instance.
- Factory Method – Delegates object creation to subclasses.
- Abstract Factory – Creates families of related objects.
- Builder – Constructs complex objects step by step.
- Prototype – Creates objects by cloning existing instances.
Core Idea
Encapsulate “how an object is created” so that client code depends on abstraction rather than construction details.
2 Structural Patterns
Structural Patterns
Focus
Structural patterns are concerned with:
How classes and objects are composed to form larger structures
They help manage relationships between components.
When to Use
Use structural patterns when:
- You need to reduce coupling between modules
- The system structure becomes rigid
- You want flexible object composition
- You need interface compatibility between incompatible components
Key Structural Patterns
- Adapter – Converts one interface into another.
- Composite – Treats individual objects and compositions uniformly.
- Decorator – Adds responsibilities dynamically.
- Facade – Provides a simplified interface to a subsystem.
- Bridge – Separates abstraction from implementation.
- Flyweight – Shares objects to reduce memory usage.
- Proxy – Controls access to another object.
Core Idea
Organize class relationships to achieve flexibility, extensibility, and minimized dependency.
3 Behavioral Patterns
Behavioral Patterns
Focus
Behavioral patterns concentrate on:
How objects communicate and collaborate
They manage responsibility distribution and interaction logic.
When to Use
Use behavioral patterns when:
- Communication logic becomes complex
- Many conditional branches exist
- You want to change behavior dynamically
- You need to reduce tight coupling in interaction flows
Key Behavioral Patterns
- Strategy – Encapsulates interchangeable algorithms.
- Observer – Defines one-to-many notification dependencies.
- Command – Encapsulates a request as an object.
- Iterator – Provides sequential access to elements.
- State – Alters behavior when internal state changes.
- Mediator – Centralizes communication between objects.
- Memento – Captures and restores object state.
- Template Method – Defines algorithm skeleton in base class.
- Visitor – Adds operations without modifying classes.
- Chain of Responsibility – Passes requests along the handler chain.
Core Idea
Encapsulate behavior and interaction patterns to reduce conditional complexity and improve flexibility.
0.5 Introducing a Design Pattern in C++
Introducing a Design Pattern in C++
Applying a design pattern in C++ should follow a disciplined engineering process rather than mechanical imitation.
5.1 General Implementation Steps
Step 1: Identify a Recurring Design Problem
Recognize symptoms such as:
- Tight coupling
- Large conditional structures
- Code duplication
- Difficulty extending functionality
- Complex object creation
- Patterns should solve actual design friction, not theoretical concerns.
Step 2: Abstract the Core Concept
Determine:
- What varies?
- What remains stable?
- Where is the dependency?
- Who owns responsibility?
Define clear abstractions (interfaces or base classes).
Step 3: Draw a UML Diagram
Visualize:
- Class hierarchy
- Relationships (inheritance, composition)
- Object collaboration
This prevents implementation errors before coding.
Step 4: Implement in C++
Translate design into C++ using:
- Abstract base classes
- Virtual functions
- Smart pointers
- RAII principles
- Dependency injection where appropriate
Avoid overusing raw pointers unless ownership semantics are clear.
Step 5: Test and Analyze
Evaluate:
- Is coupling reduced?
- Is extensibility improved?
- Did complexity decrease or increase?
- Are unit tests easier to write?
Patterns should improve architecture — not complicate it.
1.6 Deciding Whether a Pattern Is Appropriate
Deciding Whether a Pattern Is Appropriate
Not every design issue requires a design pattern.
Before applying one, ask:
- Is this problem recurring?
- Is the complexity structural or temporary?
- Will the system grow in this dimension?
- Does the solution increase abstraction unnecessarily?
Warning: Avoid Over-Engineering
Misusing patterns can lead to:
- Excessive abstraction layers
- Unnecessary inheritance hierarchies
- Reduced readability
- Performance overhead
- Increased cognitive load
Common mistake:
Implementing a pattern simply because it is known — not because it is needed.
Engineering Guideline
Use patterns when:
- The system shows clear architectural stress.
- Flexibility and extension are foreseeable requirements.
- The design problem aligns with the pattern's intent.
Avoid patterns when:
- A simple function or class solves the problem.
- Requirements are small and unlikely to change.
- The abstraction cost exceeds the benefit.
Summary
Design patterns are categorized into:
- Creational – Managing object creation
- Structural – Managing object composition
- Behavioral – Managing object interaction
Applying patterns in C++ requires:
- Problem recognition
- Proper abstraction
- Structural planning
- Careful implementation
- Critical evaluation
Patterns are tools — not mandatory architectural rules.