Design Patterns in C++: Strategy, Builder, Adapter
In high-performance systems engineering, C++ empowers developers with fine-grained control over memory management, hardware optimization, and execution speed. However, as applications scale in complexity, managing object construction, polymorphic behavior, and interface compatibility becomes critical to preventing rigid, unmaintainable codebases.
Applying proven **software design patterns** ensures your C++ architecture remains modular, extensible, and clean. This comprehensive guide covers three essential design patterns every C++ developer must master: **Strategy** (Behavioral), **Builder** (Creational), and **Adapter** (Structural).
1. Strategy Pattern: Encapsulating Algorithmic Families
The **Strategy** pattern defines a family of algorithms, encapsulates each one into a separate class, and makes their objects interchangeable at runtime. This eliminates complex conditional (`if-else` or `switch`) statements in core business logic.
#include <iostream>
#include <memory>
#include <string>
// Strategy Interface
class PaymentStrategy {
public:
virtual ~PaymentStrategy() = default;
virtual void pay(double amount) = 0;
};
// Concrete Strategies
class CreditCardPayment : public PaymentStrategy {
public:
void pay(double amount) override {
std::cout << "Paid $" << amount << " using Credit Card.\n";
}
};
class PayPalPayment : public PaymentStrategy {
public:
void pay(double amount) override {
std::cout << "Paid $" << amount << " using PayPal.\n";
}
};
// Context Class
class ShoppingCart {
std::unique_ptr<PaymentStrategy> strategy;
public:
void setPaymentStrategy(std::unique_ptr<PaymentStrategy> strat) {
strategy = std::move(strat);
}
void checkout(double amount) {
if (strategy) {
strategy->pay(amount);
} else {
std::cout << "Payment strategy not set!\n";
}
}
};
int main() {
ShoppingCart cart;
cart.setPaymentStrategy(std::make_unique<CreditCardPayment>());
cart.checkout(150.75);
return 0;
}
2. Builder Pattern: Constructing Complex Objects Step-by-Step
The **Builder** pattern separates the construction of a complex object from its representation, allowing the same construction process to create various object configurations without messy telescoping constructors.
#include <iostream>
#include <string>
class Computer {
public:
std::string cpu;
std::string ram;
std::string storage;
void specs() const {
std::cout << "CPU: " << cpu << ", RAM: " << ram << ", Storage: " << storage << "\n";
}
};
class ComputerBuilder {
Computer computer;
public:
ComputerBuilder& setCPU(const std::string& cpu) {
computer.cpu = cpu;
return *this;
}
ComputerBuilder& setRAM(const std::string& ram) {
computer.ram = ram;
return *this;
}
ComputerBuilder& setStorage(const std::string& storage) {
computer.storage = storage;
return *this;
}
Computer build() {
return computer;
}
};
int main() {
Computer pc = ComputerBuilder()
.setCPU("Intel Core i9")
.setRAM("32GB")
.setStorage("1TB NVMe SSD")
.build();
pc.specs();
return 0;
}
3. Adapter Pattern: Bridging Incompatible Interfaces
The **Adapter** pattern allows objects with incompatible interfaces to collaborate by wrapping an existing class with a new interface expected by client code.
#include <iostream>
#include <string>
// Target Interface
class MediaPlayer {
public:
virtual ~MediaPlayer() = default;
virtual void play(const std::string& audioType, const std::string& fileName) = 0;
};
// Legacy Advanced Media Library
class LegacyMediaLibrary {
public:
void playVlcFile(const std::string& fileName) {
std::cout << "Playing VLC file: " << fileName << "\n";
}
};
// Adapter Class
class MediaAdapter : public MediaPlayer {
LegacyMediaLibrary advancedMusicPlayer;
public:
void play(const std::string& audioType, const std::string& fileName) override {
if (audioType == "vlc") {
advancedMusicPlayer.playVlcFile(fileName);
}
}
};
int main() {
MediaAdapter adapter;
adapter.play("vlc", "documentary.vlc");
return 0;
}
Conclusion
Mastering software design patterns in C++—such as swapping algorithms dynamically with the **Strategy** pattern, chaining fluent initializations with the **Builder** pattern, and bridging legacy APIs with the **Adapter** pattern—enables developers to build robust, scalable, and maintainable systems.