Start with Design Patterns
Design patterns are reusable solutions to recurring problems that software developers encounter during the design and implementation of software systems. Rather than being finished code, they provide structured templates that guide how components should interact, helping engineers build systems that are robust, extensible, and easy to understand.
These patterns emerged from decades of software engineering experience and were formalized by the _Gang of Four (GoF)_ to help developers solve design challenges in a consistent and maintainable way.
At their core, design patterns help you organize complexity, making systems easier to evolve as requirements change.
Why Design Patterns Matter
Design patterns are not just theoretical ideas — they improve real-world development workflows and team collaboration.
Standardisation
Patterns provide a shared vocabulary for developers.
- Saying “use a Singleton here” communicates structure instantly.
- Teams can understand architecture decisions faster.
- Code reviews and onboarding become easier.
This shared language reduces ambiguity and improves collaboration across teams and projects.
Code Reusability
Design patterns encourage reuse of proven solutions rather than reinventing approaches.
- Reduce duplicate logic.
- Speed up development time.
- Promote modular and reusable components.
Instead of rewriting object creation logic or communication mechanisms, developers rely on established structures.
Scalability and Maintainability
Applications evolve. Patterns help systems evolve gracefully.
- Promote loose coupling.
- Improve modularity.
- Simplify extension and refactoring.
A system designed with patterns can grow without becoming fragile.
Best Practices and Proven Solutions
Patterns encapsulate solutions refined over years of engineering experience.
They help developers:
- avoid common pitfalls,
- follow architectural best practices,
- design systems that remain stable under growth and change.
Basic Concepts before Design Patterns
To visualize patterns, we use simple ASCII UML diagrams.
Class Diagram
+-------------------+
| ClassName |
+-------------------+
| - private_member |
+-------------------+
| + public_method() |
+-------------------+
Inheritance (IS-A)
<|-- indicates an inheritance arrow
+------------+ +---------------+
| BaseClass | <|-- | DerivedClass |
+------------+ +---------------+
This means that DerivedClass inherite the BaseClass
Inheritance Interface (IS-A)
<|.. indicates an inheritance interface arrow
+-----------------+ +---------------+
| InterfaceClass | <|.. | ConcreteClass |
+-----------------+ +---------------+
This means that ConcreteClass inherite the InterfaceClass interface
Association (HAS-A)
If you want to represent a usage or dependecy relationship where ClassA uses ClassB (but doesn't necessarily own it), you can use a simpler arrow notation.
+----------+ +---------+
| ClassA | --> | ClassB |
+----------+ +---------+
It indicates a dependency such as through method calls or data references.
Aggregation (HAS-A) (USE-IT)
--o indicates an Aggregation
+-----------------+ +-----------------+
| ClassA | | ClassB |
|-----------------| |-----------------|
| |o-------->| |
| | | |
+-----------------+ +-----------------+
ClassA has an aggregation relationship with ClassB
Aggregation is a weaker form of association where one class, (known as the whole), is associated with another class, (known as the part). The part can exist independently of the whole.
Composition (HAS-A) (OWN-IT)
<>-- indicates an Composition
+-----------------+ +-----------------+
| ClassA | | ClassB |
|-----------------| |-----------------|
| | | |
| |◇-------->| |
| | | |
+-----------------+ +-----------------+
ClassA has an composition relationship with ClassB
Composition is a strong form of association where one class, (known as the whole or container), owns the other class, (known as the part or component).
- ClassB cannot exist without ClassA.
- Example: House and Rooms.
When to Use Design Patterns
Design patterns are applied when you encounter recurring design problems or anticipate system growth.
They are typically categorized into three major groups.
- Creational
- Structrural
- Behavioral
Creational Patterns
Creational patterns focus on how objects are created, ensuring flexibility and efficiency.
They help decouple object creation from usage.
Common examples:
- Singleton — ensures a single instance exists.
- Factory Method — delegates object creation to subclasses.
- Builder — constructs complex objects step by step.
Example: Factory Pattern (C++)
Instead of directly instantiating objects, we delegate creation to a factory.
#include <iostream>
#include <memory>
using namespace std;
class Sensor {
public:
virtual void read() = 0;
virtual ~Sensor() = default;
};
class TemperatureSensor : public Sensor {
public:
void read() override {
cout << "Reading temperature\n";
}
};
class PressureSensor : public Sensor {
public:
void read() override {
cout << "Reading pressure\n";
}
};
class SensorFactory {
public:
static unique_ptr<Sensor> createSensor(const string& type) {
if (type == "temp") return make_unique<TemperatureSensor>();
if (type == "pressure") return make_unique<PressureSensor>();
return nullptr;
}
};
int main() {
auto sensor = SensorFactory::createSensor("temp");
sensor->read();
}
Diagram:
+----------------+
| Sensor |
+----------------+
| + read() |
+----------------+
<|--
+--------------------+ +--------------------+
| TemperatureSensor | | PressureSensor |
+--------------------+ +--------------------+
^
|
+------------------+
| SensorFactory |
+------------------+
| + createSensor() |
+------------------+
Structural Patterns
Structural patterns define how classes and objects are composed to form larger structures while keeping flexibility.
They help ensure components fit together cleanly.
Examples:
- Adapter — converts one interface into another.
- Decorator — adds behavior dynamically.
- Facade — provides a simplified interface to complex systems.
Example: Adapter Pattern (C++)
Suppose your system expects a Logger interface, but you want to reuse a legacy logger.
#include <iostream>
using namespace std;
// Target interface
class Logger {
public:
virtual void log(string msg) = 0;
};
// Legacy class
class LegacyPrinter {
public:
void printText(string text) {
cout << "Legacy: " << text << endl;
}
};
// Adapter
class PrinterAdapter : public Logger {
LegacyPrinter printer;
public:
void log(string msg) override {
printer.printText(msg);
}
};
int main() {
Logger* logger = new PrinterAdapter();
logger->log("System started");
}
Diagram
+---------+ +----------------+
| Logger |<|.. | PrinterAdapter|
+---------+ +----------------+
| log() | | log() |
+---------+ +----------------+
|
v
+----------------+
| LegacyPrinter |
+----------------+
| printText() |
+----------------+
Behavioral Patterns
Behavioral patterns manage communication and responsibilities between objects.
They improve flexibility in how objects interact and share responsibilities.
Examples:
- Observer — notifies multiple objects of state changes.
- Strategy — allows interchangeable algorithms.
- Command — encapsulates requests as objects.
Example: Observer Pattern (Embedded-style C++)
#include <iostream>
#include <vector>
using namespace std;
class Observer {
public:
virtual void update(int value) = 0;
};
class Display : public Observer {
public:
void update(int value) override {
cout << "Display updated: " << value << endl;
}
};
class Sensor {
vector<Observer*> observers;
int value;
public:
void attach(Observer* obs) {
observers.push_back(obs);
}
void setValue(int v) {
value = v;
notify();
}
void notify() {
for (auto obs : observers)
obs->update(value);
}
};
int main() {
Sensor sensor;
Display display;
sensor.attach(&display);
sensor.setValue(42);
}
Diagram
+-------------+
| Observer |
+-------------+
| update() |
+-------------+
<|--
+-------------+
| Display |
+-------------+
+-------------+
| Sensor |
+-------------+
| observers[] |
+-------------+
| notify() |
+-------------+
Use Patterns Wisely
Design patterns are powerful tools — but they are not a goal by themselves.
Use them when:
✔ a problem repeats
✔ flexibility is required
✔ the system will evolve
✔ decoupling is beneficial
Avoid them when:
✖ they add unnecessary complexity
✖ the problem is simple
✖ readability would suffer
Rule of thumb:
First make it work, then make it clean, then apply patterns if they improve design.