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

  1. Explain the Gang of Four classification of design patterns.
  2. Implement Singleton and Factory Method in C++.
  3. Use Strategy to replace type-switches with polymorphic dispatch.
  4. Connect Iterator (chapter 21) to the GoF behavioral category.
  5. 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

1. Which pattern best fits: "We have a logging system that needs exactly one instance globally"?
d. Single instance, global access — the textbook Singleton case.
2. std::stack<int> is an instance of the:
b. stack is a thin wrapper exposing a stack interface over any deque-like container.

38.13 Challenges

  1. Implement a Logger singleton with three log levels (INFO, WARN, ERROR).
  2. Refactor a switch-on-type from existing code into a Strategy.
  3. 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