Module 6 — Challenge Problems¶
Open problems. More than one defensible answer exists, and the reasoning is assessed as much as the code. Each one is designed so that a quick answer is visibly insufficient.
X1 — the flat design that might be right [L5 Design]¶
The problem. The catalogue's item family is about to grow a fourth and fifth kind, and a colleague proposes throwing the hierarchy away:
class Item {
public:
enum class Kind { Book, Journal, Dvd, EResource };
Kind kind() const;
int loanDays() const; // a switch on kind_
bool isLendable() const; // a switch on kind_
private:
Kind kind_;
std::string title_;
int pages_; // meaningless for a Dvd
std::string url_; // meaningless for a Book
};
Your job is not to reject it because "the course taught inheritance". Write the argument for the colleague, then the argument against, then a decision.
What a good answer contains.
- The size of the switch set. Count the operations that would need a
caseper kind, and project that count across the next two years of features. A design is judged by the change it makes hard, not by the state it starts in. - What the flat type cannot express. A rule that applies only to journals, an
operation only an e-resource has, a member that is meaningless for half the kinds
(the
Contractorproblem fromexamples/module-06/01-payroll-hierarchy/main.cpp). - What the hierarchy cannot express. Adding a kind you do not own: a hierarchy is closed at compile time, while a table-driven design can be extended by data. This is the strongest point the colleague has.
- The cost of each on your actual code. How many files change to add a kind in each design; how many to add a shared rule in each.
- A decision, with the condition that would reverse it. "Hierarchy, unless kinds are configured at run time" is a decision; "it depends" is not.
What would make it weak. Rejecting the flat design on principle; accepting it because it is shorter; or a verdict with no condition attached.
Deciding it for yourself. Write the verdict as one sentence that names what varies and who decides it: does the set of kinds change when you ship, or while the program runs?
X2 — refactor a hierarchy you did not write [L7 Refactor]¶
The problem. Below is a hierarchy of the kind that appears in real code: each level was written for a good reason at the time, and the accumulated result is a derived class that must neutralise three of the things it inherited.
class StreamBase {
public:
virtual ~StreamBase() = default;
virtual bool open(const std::string& path);
virtual bool close();
virtual std::size_t seek(std::size_t position); // meaningless for a socket
virtual std::size_t read(char* buffer, std::size_t n);
virtual bool flush(); // meaningless for an input stream
protected:
int fd_;
};
class InputStream : public StreamBase { … }; // can neither flush nor seek
class SocketStream : public InputStream { … }; // no path, no seek, no size
What a good answer contains.
- Three separated interfaces —
Readable,Writable,Seekable— with the stream classes implementing only what they honour. Note that this is Module 8's material used as a refactoring tool, and that the interfaces are small on purpose. - The refactoring expressed in steps that keep the code working: introduce the interface as a base alongside the old one, make callers depend on the interface, then remove the operation from the old base. A refactor that breaks the build for a week is a rewrite, not a refactor.
- A list of what each caller loses. If some caller genuinely used
seekon a socket, you have found a design question the hierarchy was concealing. - The cost, named honestly: more types, and a virtual call on the interface instead of a direct one. Say whether that matters here — for I/O, no; for a per-pixel loop, yes.
What would make it weak. Splitting the interfaces but leaving StreamBase as a
god class "for compatibility" with no plan to remove it; naming the interfaces after
implementations (SocketStreamInterface); or a before/after that omits the call
sites.
X3 — when is a deep hierarchy the right answer? [L8 Evaluate]¶
The problem. "Prefer shallow hierarchies" is repeated so often that it is
believed rather than applied. Find a hierarchy — the standard library's
ios_base → basic_ios → basic_ostream → ostream → ofstream, a GUI toolkit, a
compiler's AST, or one of your own — and argue for its depth.
What a good answer contains.
- What each level contributes. If a level contributes only convenience
constructors, that is a point against. If it contributes a policy that every level
below obeys —
basic_iosowns the stream state, and every formatted operation depends on it — that is a point for. - A count of the operations each level adds, and the number of classes affected by a change at each level. Depth is expensive in proportion to what a change at the root forces on the leaves.
- Evidence, from the interface rather than from opinion: are there derived classes
that cannot honour an inherited operation? Does the hierarchy use
virtualdestructors consistently? Does it useprotecteddata that a leaf could corrupt? - A verdict with its conditions. "Keep four levels because levels 2–4 are policy, and flatten level 5 because it only forwards" is a usable answer. "Deep hierarchies are bad" is not, and neither is "it works, so it is fine".
How you will be judged. Whether someone who disagrees can still follow your reasoning, whether your evidence comes from the code rather than from a book, and whether you said what would change your mind.
The rubric¶
Applied to X1, X2 and X3 alike. Score each row 2 (done), 1 (present but weaker), 0 (absent).
| Criterion | |
|---|---|
| 1 | The problem is restated in one sentence, in your own words, before any solution appears |
| 2 | Every claim about behaviour is checkable — a program, an output, a compiler message, or a named source |
| 3 | The design decision is stated as a decision, with what it costs |
| 4 | At least one alternative is described fairly enough that its advocate would recognise it |
| 5 | The condition that would reverse your decision is named |
| 6 | The answer says what a reader can do next, not only what is true |
X1 and X3 have no "correct" verdict, so rows 2 and 4 carry most of the marks. X2 has a correct shape, and row 3 decides between a refactor and a rewrite.
Evaluating your own answer¶
Write these three lines before reading any suggested approach:
Then compare against the "what a good answer contains" list, item by item, and mark each one present, missing, or present but weaker than the list. The third category is where the marks are: an answer that mentions substitution but cannot say what breaks without it has the vocabulary and not the model.
Two questions to close with:
- Could a reader who disagreed with you still follow your reasoning? If your answer only works for someone who already agrees, it is a preference, not an argument.
- What would make you change your mind? A concrete observation — a new kind added without recompiling, a derived class that cannot honour an operation — not "more experience".
Next: Self-assessment — nine questions, including two predictions and one classification task. · All modules