Module 15 — Self-Assessment¶
Ten questions. Part A checks vocabulary and comprehension; Part B checks that you can judge — which is what this module is for, and the only part that tells you whether you can use it.
Answer on paper, then mark yourself and note which section of Concepts each miss points back to. Several answers are judgement calls; for those, what is marked is whether your answer states a cost and a rejected alternative, not whether it matches a preferred design.
Target: ≥ 80%, with every miss explained. Below 60%, redo sections 1–8 before Lab 15 — sections 9–14 are the pattern vocabulary, which is the easier half.
Part A — Recall and comprehension¶
-
What is this module's criterion for a good design, and how is it measured? (Section 1. Change localisation: pick a plausible next requirement, trace it through the code, and count the places it touches. Design quality here is a number, not an adjective.)
-
Name the four relationships a UML class diagram distinguishes beyond inheritance, and say what separates composition from aggregation. (Section 2. Dependency, association, aggregation, composition. Composition means the part's lifetime is the whole's; aggregation means the part can outlive the whole. In code, that is the difference between a
vector<Item>and avector<shared_ptr<Item>>.) -
State the single-responsibility test, and say when splitting a class is the wrong answer. (Section 3. One sentence without "and" — and more precisely, one audience with one reason to change. Splitting is wrong when the two clauses have the same audience and the same rate of change: then the extra types are cost with no buyer.)
-
What does "closed for modification" mean, and what does it cost? (Section 4. Closed with respect to what varies — a new kind is a new class, with no edit to existing classes. The cost is the other axis: adding a new operation that depends on the kind now edits the interface and every implementation. You choose the axis, and the requirements should choose it for you.)
-
What are the three promises a derived class must keep, and where does the bug appear when one is broken? (Section 5. Preconditions may not be strengthened, postconditions may not be weakened, invariants must be preserved. The bug appears in client code written against the base — not in the derived class, which may be behaving perfectly reasonably given its own nature. The fault is in the abstraction.)
-
What does interface segregation protect against, and what does obeying it cost? (Section 6. It protects implementers from writing operations they cannot honour, and clients from depending on operations they never call. The cost is more types and, sometimes, interfaces with one reason between them; and every implementer must be found and updated when an interface it implements changes.)
-
State dependency inversion, including who owns the interface, and explain what it has to do with testing. (Section 7. High-level policy and low-level detail both depend on an abstraction, and the policy owns the interface, not the implementation. When the policy owns it, a test double can implement it, so the policy's logic becomes testable — which is usually the first symptom that inversion is missing: "my tests need a real database".)
-
Name two SOLID principles that commonly conflict, and say what you do when they do. (Section 8. Any of: single responsibility vs simplicity; interface segregation vs single responsibility; open/closed vs readability; dependency inversion vs knowing what runs; Liskov vs reuse. What you do is choose, and state the cost, the rejected option and the condition that would reverse the decision.)
-
When is a design pattern the wrong answer? (Sections 9 and 13. When the variation it abstracts is not real — no second implementation exists and none is required; when a plain function or a template parameter is simpler; when the "strategy" carries one value; when the factory wraps a single constructor. A pattern applied in anticipation is a bet that the future is shaped like the pattern.)
-
What must an observer design answer besides "how do I notify"? (Section 12. Who determines that an observer is still alive, and what happens at the moment it stops being — weak references with pruning, RAII unregistration, or subject-owned subscribers, each with a cost. The pattern does not create this question; it relocates it.)
Part B — Prediction and judgement¶
-
Predict, then check. Two designs for the same system. Design A has
ItemKindas anenumand aswitchover it inloanPeriodDays,maxRenewalsanddescribe. Design B has anIteminterface with those three operations as pure virtual, and three concrete kinds. A fourth kind is now required, and separately, a fourth operation that depends on the kind is required.For each design, say how many places must be edited for the new kind, and how many for the new operation. Which design is better here, and what would change your answer?
Full credit: A — new kind: the enumerator plus three new
caselabels, four edits in one file, and the risk of missing one is real. New operation: one function, oneswitch, one file. B — new kind: one new class, finished, with no edit to existing classes (plus one registration line if a factory exists). New operation: the interface plus every implementation, so four edits. Neither is better in the abstract; the requirements decide, and the deciding question is which axis has been changing. For a system where kinds arrive and are stable in number with a fixed set of operations, A is the better design and B is over-engineering — which is the harder judgement, and the one this module wants. -
Predict what this program prints, and whether the output is correct.
#include <iostream>
#include <memory>
#include <string>
#include <vector>
class Notifier {
public:
virtual ~Notifier() = default;
virtual void send(const std::string& message) = 0;
};
class Desk {
public:
explicit Desk(Notifier& notifier) : notifier_(notifier) {}
void lend(const std::string& title) { notifier_.send("lent " + title); }
private:
Notifier& notifier_;
};
int main() {
struct Counter {
int sends = 0;
void send(const std::string& m) { ++sends; std::cout << "(" << sends << ") " << m << "\n"; }
} counter;
// the question is about this line, not about the class above
Desk desk{*reinterpret_cast<Notifier*>(&counter)};
desk.lend("Compilers");
}
Full credit: it compiles, it is undefined behaviour, and it may print anything, crash, or appear to
work. Counter is not derived from Notifier; it happens to have a send method, and
reinterpret_cast throws away the type system's only defence. The correct version is a Counter that
publicly derives from Notifier and overrides send — or, if the real requirement is "any callable",
std::function<void(const std::string&)>, which is a decision about the interface rather than a cast.
The point of the question: dependency inversion is enforced by the type system only if you take the
interface seriously. A Notifier& parameter bought nothing here, because the caller reached around
it.
-
Judge this decision, and improve it.
"We used a singleton for the catalogue, because there is only one library and we wanted every class to be able to reach it without passing it around."
Full credit: the diagnosis is that the second clause is the actual motivation — convenience of access, not uniqueness — and that convenience is being paid for with hidden global state. The consequences to name:
Catalogue::instance()appears deep inside business logic, so a component's dependencies are invisible in its signature; tests cannot isolate, because the same catalogue persists between them and there is no way to substitute a double; construction order between singletons becomes a run-time hazard; and thread safety is now the singleton's problem. The alternative: one catalogue, created once at the composition root and passed to whoever needs it — which also makes the "only one" claim true by construction rather than by convention. A good answer also allows for the case where a singleton is defensible (a genuine process-wide resource with no substitutable behaviour), names what makes it defensible, and says what it costs there. -
Judge this one too. A
LibraryServiceinterface has six operations. ASnapshotCatalogueimplementer honours two and writes four stubs that throwstd::logic_error("not supported"). The author's defence: "The interface is one cohesive concept, and the throw documents the contract."Full credit: name what the defence gets right — a throwing stub is at least honest, unlike a silent no-op, and the operations do belong to one domain. Then name what it costs: every implementer must write code it cannot honour; every client depends on four operations it may never call; and the failure is moved to run time, where a caller learns the limit by crashing (or, worse, by a caught exception the caller was never told to expect). The fix is a capability split — a read-only interface the snapshot implements, and a lending interface the desk implements — after which the wrong substitution becomes a compile error, which the worked example checks with
static_assert. A complete answer notes the cost of splitting: more types, and a caller who now names two interfaces to do two things. -
The one question that matters most. Name a place in your own Lab 14 system where you would remove an abstraction, or would deliberately not add one that the requirement does not yet justify. Write it as the form at the end of the concepts page: decision, the requirement it was made for, the cost of that choice, the option you rejected and what it would have cost, and the condition that would reverse it.
Full credit: a real place, a real requirement, a named cost, and a reversal condition. Partial credit for a genuine attempt with a weak cost. No credit for "my design is fine so far" — every first design has one of these, and finding it is the point of the module's last example. Compare
examples/module-15/07-over-abstraction/main.cpp, which builds the same small requirement twice, once as a function and once across five types, prints the reader cost of the second, and names the conditions under which it would be right.
Marking yourself¶
| Part | Questions | What a mark means |
|---|---|---|
| A | 1–10 | One mark each, for the substance of the answer rather than its phrasing |
| B | 11 | A kind count of 4 and an operation count of 4, and a decision made from the requirements |
| 12 | Undefined behaviour identified, and the type-safe alternative named | |
| 13, 14 | A cost named for the choice, and for the rejected alternative — plus something the defence got right | |
| 15 | A real decision from your own code, with a cost and a reversal condition |
12 out of 15 is a pass, and question 15 is not optional: the exit level of this module is 8, and evaluating your own design is the whole of it.
Then run this audit, which is the part a quiz cannot check:
- [ ] I can state each class's responsibility in one sentence, without "and"
- [ ] I can draw my design's UML diagram from the code, and every relationship and multiplicity matches
- [ ] I can name, for every pattern in my system, the requirement that justified it
- [ ] I can name at least one pattern I considered and declined, with the reason
- [ ] I have a before-and-after change-cost count for at least one refactoring, as numbers
- [ ] I can name two of the five principles that pulled against each other in my own work
- [ ] I have one written decision that includes a cost for the option I chose
- [ ] I can name one decision I would reverse, and what would have to change for me to reverse it
If any box is unticked, the corresponding section of Concepts has it: the diagram notation is section 2, the responsibility sentence is 3, the pattern justifications are 9–13, the change-cost trace is in Debugging Move 1, the tensions are 8, and the evaluation form is 14.
Continue¶
Previous: Challenge problems · Next: Summary · Module home: Module 15