Module 15 — Misconceptions and Mistakes¶
Two different problems, fixed two different ways.
A misconception is a wrong mental model, and only evidence removes it — so each one below is paired with a program or a measurement that contradicts it. A mistake is a concrete error, and information fixes it — so each one below comes with the message this toolchain actually printed and the corrected design.
Every diagnostic quoted here was produced by compiling the snippets shown, with the course's flags.
Part 1 — Misconceptions¶
1. "A design pattern is a goal to be achieved."¶
Patterns are taught as good things, so using more of them feels like doing better work. The evidence against that is in this module's last example: the same requirement, implemented as a four-parameter function and as five collaborating types, both correct — and the second one costs a reader four types and about sixty lines before they can answer "what is the fine for a staff DVD?".
A pattern is vocabulary for a problem that exists. Name what you built; do not build in order to name it.
2. "More abstraction is always better design."¶
Abstraction hides detail, and hiding looks like a virtue until you need the detail. Every interface is a thing a reader must find, understand and hold in their head, and every injection site is a place the actual behaviour is decided somewhere else. The test is not "is it abstract?" but who benefits from this indirection, and what does it buy them? If the answer is "future flexibility" without a named future requirement, the answer is no.
3. "SOLID principles can all be maximised at once."¶
They pull against each other, and a design that obeys every principle maximally is unusable: a type per behaviour, an interface per method, an injection per dependency. The skill is choosing with the cost stated:
"We kept
FineCalculatorconstructing its own rates because there is exactly one configuration, and we accept that a test cannot vary the rate."
That is not a weaker answer than "injected everything". It is a design decision with a named cost and a trigger for revisiting — which is what section 8 of the concepts page is about, and what Lab 15's design report marks.
4. "Inheritance is for reuse."¶
It is the most common motivation people state, and it produces exactly one failure mode:
class AnythingLendable { public: virtual int loanPeriodDays() const = 0; };
class ReferenceCopy : public AnythingLendable {
public:
int loanPeriodDays() const override { throw std::logic_error("never lent"); }
};
The parallel arrays and the shared title() were the reuse. The loanPeriodDays() was a promise the
base made on behalf of types it had never met, and the client code that relied on it now fails at run
time for one input. Inheritance is for substitution: Derived must be usable everywhere the base is,
including by code written before Derived existed. Reuse without substitution is composition.
5. "A class should be as small as possible."¶
Single responsibility sounds like smallness, so students split until nothing means anything: a
LoanFineRater, a LoanFineRounder, a LoanFineFormatter, each with one method and one caller, none
of which is a concept anyone would say out loud. The test is not size — it is how many audiences would
change it, and for how many reasons:
If the one-sentence statement of responsibility needs "and", there are two responsibilities.
If the sentence is "it holds the fine rate and computes the fine", the two clauses change together for one audience and the split is noise. Note also the second half of the test in the example program: split when the clauses have different audiences or different rates of change.
6. "Interfaces should be defined by the implementation."¶
The implementation usually exists first, so its interface is written from what it can do. That is the dependency pointing the wrong way, and it has two symptoms:
- clients depend on the union of everything every implementation can do, so a change to
emailMemberforces a report generator to be recompiled and re-tested; - a policy that needs only "send this message" gets an interface with nine operations on it, and its tests must supply all nine.
Write the interface from the caller's side, and put it where the caller lives. The question test: whose header includes whose?
7. "Patterns make code easier to change."¶
They make one kind of change easier — a new implementation of the variation they abstract — and every other change harder, because there is more code to read through. Strategy removes a conditional and adds a type, a parameter and a decision about where the strategy is chosen. Observer decouples notification and relocates the lifetime question (see mistake M3 below). Both are worth it, and the word "worth" is the one that has to be defended with a requirement.
8. "If a design uses many patterns it is mature."¶
Maturity is visible in the absences: no factory for the one thing constructed in one place, no strategy for a rule that is one number, no observer for the single thing that needs to know. A design that can explain why it declined three patterns is more sophisticated than one that contains five — and the second is much easier to write, which is why the first requires the rejection note.
9. "Evaluation is subjective, so any design is defensible."¶
Judgement has no formula, and it does have criteria: a stated context, the cost of the chosen option, a real alternative, and a condition that would reverse the decision. Applied to two concrete designs, that usually produces a clear preference — and where it does not, it produces a clear statement of what the preference depends on. "It is a matter of taste" is what evaluation looks like before it has been done.
Part 2 — Common programming mistakes¶
Symptoms, and what actually causes them¶
| Symptom | Cause |
|---|---|
| A new kind of item requires editing five files | A conditional on the kind, in several places: the variation is data, not behaviour |
error: cannot declare variable 'x' to be of abstract type after adding a function to an interface |
Every implementer now has a pure virtual it does not implement — often the fat-interface symptom arriving all at once |
| Compile errors in classes that only use an interface, after one implementation changed | A fat interface: users depend on more than they need |
| A derived class cannot implement an inherited operation sensibly | A Liskov violation: the hierarchy was chosen for reuse |
A dynamic_cast chain replacing an if on a kind |
The variation was moved, not removed; the cast sequence is the design asking for a virtual operation |
warning: deleting object of abstract class type ... which has non-virtual destructor |
A missing virtual destructor on an interface — Module 7's rule, now that factories return interfaces |
| A dangling observer notified after destruction | Registered and never unregistered (or held by a raw pointer): a lifetime that nobody owns |
| Memory grows as observers register repeatedly | Duplicate registration, or no unregistration path |
| A factory returning raw pointers that nobody owns | Creation was decoupled; ownership was not expressed |
| Two classes become mutually dependent after injecting an interface | The interface was placed next to the implementation instead of next to the policy |
| A "strategy" with one implementation and one integer inside it | A pattern applied to a variation that does not exist |
| The design report names no weakness | No honest evaluation was done: every design of this size has one |
Worked mistakes¶
M1. Adding an operation to an interface breaks every implementer at once¶
class LibraryService {
public:
virtual void addItem(const std::string& title) = 0;
virtual std::vector<std::string> allTitles() const = 0;
virtual void printReceipt(const std::string& title) = 0; // added later
};
class ReadOnlyCatalogue : public LibraryService {
// implements the first two, and cannot implement the third
};
error: cannot declare variable 'catalogue' to be of abstract type 'ReadOnlyCatalogue'
note: because the following virtual functions are pure within 'ReadOnlyCatalogue':
note: 'virtual void LibraryService::printReceipt(const std::string&)'
This error is a design report. The compiler lists exactly what the class cannot do, and it lists it for every implementer of that interface. If the list of broken classes is long, the interface was doing too much — the fix is not to write stubs, it is to split the interface so that each implementer implements only what it can honour.
M2. An interface without a virtual destructor¶
class CatalogueItem {
public:
virtual std::string title() const = 0;
// no virtual destructor
};
CatalogueItem* item = new Book("Compilers");
delete item;
warning: deleting object of abstract class type 'CatalogueItem' which has non-virtual destructor
will cause undefined behavior [-Wdelete-non-virtual-dtor]
The warning names the whole problem: delete through a base pointer destroys the base and does not
run the derived destructor, so anything the derived class owns leaks. In this module the mistake
arrives through a new door — a factory that returns unique_ptr<CatalogueItem> — because the ownership
is now managed by something that only sees the base type. Every interface needs
virtual ~Interface() = default;.
M3. The dangling observer¶
class Board {
std::vector<Subscriber*> subscribers_; // raw pointers, no ownership
void subscribe(Subscriber* s) { subscribers_.push_back(s); }
void announce(const Item& item) {
for (Subscriber* s : subscribers_) { s->onAvailable(item); }
}
};
{ EmailAlert alert("ayesha@example.edu"); board.subscribe(&alert); }
board.announce(item); // calls a destroyed object
No diagnostic, because nothing here is ill-formed — the code is simply undefined behaviour when it runs. It usually works, which is why it survives review: the dead object's bytes are still there and the virtual call lands in the right vtable.
examples/module-15/05-observer-and-lifetimes/main.cpp therefore does not run this version; it
describes it and then implements the three designs that are safe, with the cost of each:
| Design | What it costs |
|---|---|
| The subject holds weak references and prunes | a lock() per notification; subscribers must be shared_ptr-owned |
| The subscriber unregisters in its destructor (RAII token) | the token must be kept; the destructor must not throw |
| The subject owns its subscribers | nobody outside can subscribe — not a plugin, not a test double |
Patterns do not remove the ownership questions; they relocate them. This is Module 9's dangling reference wearing a pattern's name, and the diagnosis is the same one: who owns this, and for how long?
M4. A conditional replaced by a dynamic_cast chain¶
for (Item* item : items) {
if (auto* book = dynamic_cast<Book*>(item)) { /* book handling */ }
else if (auto* dvd = dynamic_cast<Dvd*>(item)) { /* dvd handling */ }
else if (auto* equipment = dynamic_cast<Equipment*>(item)) { /* ... */ }
}
It compiles, it has no switch, and it is the same design: the kind is still being interrogated, now
in a slower and more fragile form (a new kind is a new branch that silently does nothing if it is
forgotten). The sequence of casts is the design asking for a virtual operation. If the operations
genuinely differ per kind and belong to the kind, put them in the interface; if they belong to the
caller, a visitor is the honest name for what you are building.
M5. A "factory" that returns a raw pointer nobody owns¶
The creation was decoupled and the ownership was not expressed, so every caller must guess — and the guesses disagree, which is how double frees and leaks appear in the same program. A factory's return type is a design decision:
std::unique_ptr<Item> createItem(const std::string& kind); // the caller owns it
std::shared_ptr<Item> createItem(...); // ownership is shared
Item createItem(...); // a value: no ownership question
Choose one deliberately, and say why in the header. Module 11 covers the language; this module is about the fact that the factory is where that decision is made on behalf of every caller.
The four questions worth memorising¶
- If this requirement arrived, how many files would it touch? A number above two or three is a design finding.
- Does the one-sentence responsibility of this class need "and"?
- Which interface does this client actually use, and who owns it?
- What does this pattern cost, and what would make me remove it?
Continue¶
Previous: Worked examples · Next: Debugging · Module home: Module 15