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