37 Object-Oriented Design Patterns
38 Object-Oriented Design Patterns
Week 8, Session 1. CSS 343.
38.1 Warmup
Palindrome Number (#9). Yes, again. Quick warmup. Then we leave algorithms and talk about how to structure programs.
38.2 Learning objectives
- Explain the Gang of Four classification of design patterns.
- Implement Singleton and Factory Method in C++.
- Use Strategy to replace type-switches with polymorphic dispatch.
- Connect Iterator (chapter 21) to the GoF behavioral category.
- Identify which pattern best addresses a problem description.
38.3 What is a design pattern?
A design pattern is a reusable solution to a recurring OOP design problem. The 1994 Gang of Four book (Gamma, Helm, Johnson, Vlissides) cataloged 23 of them.
Patterns are not algorithms. They are templates for organizing code. The book is still relevant 30 years on because the problems are the same.
GoF organizes patterns into three categories:
| Category | Concern | Examples |
|---|---|---|
| Creational | How objects are made | Singleton, Factory Method, Abstract Factory |
| Structural | How objects compose | Adapter, Decorator, Composite |
| Behavioral | How objects interact | Strategy, Observer, Iterator |
We will cover a handful that come up most in practice.
38.4 Singleton
Guarantees exactly one instance and provides global access.
class Settings {
public:
static Settings& getInstance() {
static Settings instance; // C++11 guarantees thread-safe init
return instance;
}
int getVolume() const { return volume_; }
void setVolume(int v) { volume_ = v; }
// delete copy and move
Settings(const Settings&) = delete;
Settings& operator=(const Settings&) = delete;
private:
Settings() : volume_(50) {}
int volume_;
};Usage: Settings::getInstance().setVolume(80);.
Singletons are global state in a tuxedo. They are convenient and dangerous. Overuse couples your code to the singleton’s existence, making testing and modular reasoning harder. Use them sparingly — for things like logging, configuration, hardware abstractions.
38.5 Factory Method
A method that returns objects of a varying type, hiding which concrete class is instantiated.
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
Circle(double r) : r_(r) {}
double area() const override { return 3.14159 * r_ * r_; }
private:
double r_;
};
class Square : public Shape {
public:
Square(double s) : s_(s) {}
double area() const override { return s_ * s_; }
private:
double s_;
};
class ShapeFactory {
public:
static unique_ptr<Shape> create(const string& kind) {
if (kind == "circle") return make_unique<Circle>(1.0);
if (kind == "square") return make_unique<Square>(1.0);
return nullptr;
}
};Adding a new shape changes the factory but not the callers of create.
38.6 Strategy
Replace a chain of if-else on type with polymorphism. The behavior is a parameter.
class SortStrategy {
public:
virtual void sort(vector<int>& v) = 0;
virtual ~SortStrategy() = default;
};
class BubbleSorter : public SortStrategy {
public:
void sort(vector<int>& v) override { /* bubble sort */ }
};
class MergeSorter : public SortStrategy {
public:
void sort(vector<int>& v) override { /* merge sort */ }
};
class Sorter {
public:
Sorter(unique_ptr<SortStrategy> strategy) : strategy_(std::move(strategy)) {}
void sort(vector<int>& v) { strategy_->sort(v); }
private:
unique_ptr<SortStrategy> strategy_;
};Now Sorter has no idea how sorting is done — it just calls strategy_->sort. Want to swap the algorithm at runtime? Pass a different strategy. The user code does not branch on type.
Every time you write a switch on an enum to do “the right thing” for a type, you are reimplementing a Strategy pattern poorly. The OOP-clean version is one virtual call.
38.7 Observer
A subject publishes events; listeners subscribe and are notified.
class Listener {
public:
virtual void onEvent(const string& event) = 0;
virtual ~Listener() = default;
};
class EventSystem {
public:
void subscribe(Listener* l) { listeners_.push_back(l); }
void publish(const string& event) {
for (auto* l : listeners_) l->onEvent(event);
}
private:
vector<Listener*> listeners_;
};GUI frameworks (Qt, MFC) are built around Observer. So are game engines, message queues, and React.
38.8 Adapter
Wrap an interface to look like another.
class LegacyPrinter { // pre-existing, cannot change
public:
void emit(const char* text);
};
class IPrinter { // our codebase's interface
public:
virtual void print(const string& s) = 0;
virtual ~IPrinter() = default;
};
class LegacyAdapter : public IPrinter {
public:
LegacyAdapter(LegacyPrinter* p) : p_(p) {}
void print(const string& s) override { p_->emit(s.c_str()); }
private:
LegacyPrinter* p_;
};The adapter translates between two interfaces without modifying either side.
std::stack<T> is an adapter over std::deque<T> — the underlying container can be anything supporting push_back/pop_back/back.
38.9 Iterator (revisited)
We covered iterators in Chapter 21. The pattern: provide a way to traverse a collection without exposing its internals. Every begin()/end() in the STL is the Iterator pattern.
38.10 When to apply patterns — a warning
Design patterns are a vocabulary. They let you say “use Observer here” instead of explaining the whole structure. They are not a checklist. Forcing patterns into a small program inflates code without benefit. The right time to introduce a pattern is when you find yourself solving the same structural problem three times.
38.11 Try it
Take a project where you have if (type == "X") ... else if (type == "Y") ... branching on a string or enum, doing different things per branch. Refactor using Strategy. Notice how the call site simplifies to one virtual call.
38.12 Self-check
std::stack<int> is an instance of the:stack is a thin wrapper exposing a stack interface over any deque-like container.
38.13 Challenges
- Implement a
Loggersingleton with three log levels (INFO, WARN, ERROR). - Refactor a switch-on-type from existing code into a Strategy.
- Build a simple Observer-pattern event bus. Test by subscribing two listeners and publishing one event.
38.14 Where this fits
You can now organize a program of any size. The next chapter goes deeper into the underlying C++ mechanism — virtual functions, the vtable, and modern smart pointers.
| You are here | Coming up |
|---|---|
| GoF patterns: Singleton, Factory, Strategy, Observer, Adapter | Chapter 36: polymorphism and the vtable |