Skip to content

Module 15 — SOLID and Design Patterns

After this module you can document a design with UML, explain why it is structured the way it is, apply three patterns idiomatically in C++, and recognise when a pattern would be over-engineering.


At a glance

Prerequisites Module 14
Estimated study time 9–11 hours
Lab Lab 15 — refactor to patterns, with a UML diagram
Exercises Three tiers — warm-up, core, challenge
Cognitive levels L5 Design → L8 Evaluate — exit level L8 Evaluate
Self-assessment Module quiz

Why this module matters

Everything so far has been about making correct code. This module is about making code that survives change — which is the actual reason object orientation was invented.

Two bodies of knowledge help. SOLID names five design principles that keep classes easy to change, in the vocabulary of the features you already know: cohesion, coupling, interfaces, and dependency. Design patterns are named solutions to problems that recur so often they have become vocabulary — "use a strategy here" communicates a shape in two words.

C++ has its own flavour of both. Since ownership is explicit (Module 11), a pattern in C++ says exactly who owns what; a factory returns a unique_ptr, an observer holds callables in a container, and a decorator wraps a wrapped object whose lifetime it controls.

The module deliberately teaches the counter-case too. A pattern applied where a plain function would do is a cost with no benefit: more types, more indirection, more places to look. You will practise recognising that, because a course that only teaches patterns produces graduates who over-engineer everything.


Learning objectives

By the end of this module you will be able to:

  1. Read and draw a UML class diagram: classes, attributes, operations, visibility, associations, aggregation, composition, inheritance and interfaces, with multiplicity.
  2. Move from requirements to design: identify responsibilities, assign them to classes, and document the result before writing code.
  3. State each of the five SOLID principles and apply at least three of them to a real refactor in this course's own code.
  4. Explain the difference between depending on an interface and depending on a concrete implementation, and why the first is more changeable.
  5. Describe, in problem-and-consequences terms, what each studied pattern is for:
  6. Creational: factory method, singleton (with its problems)
  7. Structural: adapter, decorator
  8. Behavioural: strategy, observer, template method
  9. Implement those patterns idiomatically in C++17, with correct ownership and no leaks.
  10. Use dependency injection through a constructor parameter so a component can be tested without its real collaborator.
  11. Replace the switch-on-type design from Module 7 with a strategy, and compare the two implementations.
  12. Explain why the singleton is frequently an anti-pattern, especially for testability, and name an alternative.
  13. Judge, for a stated requirement, when a pattern is not warranted, and write that judgement down.

Core concepts

  • UML class diagram — standard notation for classes and their relationships. Worth learning because it is language-independent and universally understood.
  • Multiplicity — 1, 0..1, *: how many objects participate in a relationship.
  • Visibility — + public, - private, # protected.
  • Responsibility — what a class is accountable for. The unit of design.
  • SOLID:
  • Single responsibility — a class should have one reason to change.
  • Open/closed — open to extension, closed to modification.
  • Liskov substitution — a derived type must be usable wherever its base is promised, without surprising the caller.
  • Interface segregation — many narrow interfaces beat one wide one.
  • Dependency inversion — depend on abstractions, not on concrete details.
  • Dependency injection — supplying a collaborator from outside (usually through a constructor) instead of creating it inside. Makes substitution and testing possible.
  • Design pattern — a named, reusable solution to a recurring design problem. Not a library and not a language feature.
  • Factory method — a function that creates objects, so callers depend on the interface rather than on constructors of concrete types. Returns a smart pointer (Module 11).
  • Singleton — a class with exactly one instance and a global access point. Understandable, over-used, and hostile to testing; prefer passing a dependency.
  • Adapter — wraps an existing type to present a different interface the rest of the system expects.
  • Decorator — wraps an object to add behaviour while keeping the same interface, without changing the wrapped class. Ownership is explicit: the decorator holds the object it decorates.
  • Strategy — encapsulates interchangeable behaviour behind an interface so it can vary independently of the context that uses it. The run-time counterpart of a template parameter (Module 12).
  • Observer — lets subscribers be notified of an event without the subject knowing their concrete types. Implemented with a container of callbacks; watch for lifetime problems when subscribers are destroyed.
  • Template method — a base class defines an algorithm's skeleton, with steps it lets derived classes supply. Related to the non-virtual interface idiom from Module 8.
  • Over-engineering — adding abstraction for a change that has not happened. The most common form of this in student code, and in industry code.
  • YAGNI — "you aren't gonna need it": the counterweight to speculative generality.

Module contents

Section What it contains
Introduction (this page) Overview, prerequisites, objectives, concept inventory
Concepts and explanations UML notation, each SOLID principle, and each pattern as problem → forces → solution → consequences
Worked examples The running system refactored twice: once with a strategy, once with an observer
Code examples examples/module-15/ — seven programs, each with captured output
Misconceptions and mistakes Wrong mental models about design — "a pattern is a goal", "more abstraction is always better", "inheritance is for reuse" — and the mistakes that follow from them
Debugging Diagnosing a design that will not hold: counting what a requirement touches, reading a class diagram for trouble, and finding the variation nobody named
Exercises Three tiers, eleven tasks — from drawing the design you have to restructuring a working subsystem so it is open for extension
Lab 15 Patterns applied to the running system, documented with a UML diagram — and the patterns deliberately not used, with the reason
Challenge problems Open-ended design prompts, including "no pattern needed here — explain why"
Self-assessment Questions with explained answers, and a re-test schedule for what you got wrong — then the Module quiz
Summary The module compressed to what you must retain, and what to do before you move on
Further reading Books and official documentation

What you can do after this module

  • Produce a UML class diagram for a non-trivial design and defend every relationship on it.
  • Refactor existing code to satisfy SOLID, and show the improvement with before and after versions.
  • Implement a factory, a strategy and an observer in C++ with correct ownership and no leaks.
  • Inject a dependency so a component can be tested without its real collaborator.
  • Write one paragraph explaining why you deliberately did not introduce a pattern somewhere.

Where this is used later

  • Everything here is applied at once in the capstone, Module 16, which requires a documented design, a refactor, and tests.
  • UML diagrams are the required documentation format for the capstone report.
  • Dependency injection is what makes the capstone's components testable.

Common mistakes to watch for

Symptom Cause
A class is named Manager and has twelve unrelated functions Single responsibility violated; the class is a destination for code, not a design
Every new feature edits the same if/else chain Open/closed violated. Introduce polymorphism or a strategy
A derived class throws NotSupported for an inherited operation Liskov substitution and interface segregation both violated. The interface is too wide
A class constructs its own collaborators internally Dependency inversion violated; nothing can be substituted or tested
Tests need a real database, file system or network Dependencies are hard-wired instead of injected
Singleton::instance() is called deep inside business logic Hidden global state: hard to test and impossible to reset between tests
An interface exists with exactly one implementation and no prospect of another Speculative abstraction. Not automatically wrong, but it should be a decision, not a reflex
An observer list holds references to destroyed subscribers Subscribers outlived their subscription. Manage the subscription's lifetime explicitly
A decorator leaks or deletes twice Two objects believe they own the wrapped object. The decorator owns it, exactly once

Skills checklist

Levels this module works at: [L5 Design → L8 Evaluate] — see How you will learn.

Tick an item only when the evidence exists:

  • [ ] I can draw a UML class diagram from code rather than from intention — evidence: a diagram with multiplicities, checked declaration by declaration, where every stored vector<Item> is marked composition and every stored reference is an association
  • [ ] I can measure a design with a count instead of an adjective — evidence: one requirement traced to a number of files and sites, before and after a refactoring
  • [ ] I can state a class's responsibility in one sentence and act on the ones needing "and" — evidence: the sentences, and either the split or the written reason not to split
  • [ ] I can replace a growing conditional with polymorphism and prove the existing code is closed — evidence: a new kind added as one class plus one registration line, with an empty diff to the existing files
  • [ ] I can find a Liskov violation and fix the abstraction rather than the override — evidence: a capability interface that makes the wrong substitution a compile error
  • [ ] I can split a fat interface so that no implementer writes a stub — evidence: two implementers with zero stubs, and a caller that cannot be handed the wrong pair
  • [ ] I can invert a dependency and write a test that needed it — evidence: an injected collaborator, a test double, and an assertion the old design could not have supported
  • [ ] I can implement factory, strategy and observer in C++ with correct ownership — evidence: a factory returning unique_ptr, a strategy injected where the context is known, and an observer whose lifetime question is answered in the design
  • [ ] I can write down a pattern I considered and declined, with the reason — evidence: the rejection note, naming the requirement that was not real yet
  • [ ] I can evaluate my own design — evidence: one decision in the form context, choice, cost, rejected option, condition that would reverse it, ending in a decision I would reverse

Continue

Previous: Module 14 — The STL with OOP · Next: Module 16 — Advanced OOP Capstone · All modules: Modules index