Module 8 — Lab 8: the catalogue's contract¶
Levels: [L4 Analyze → L6 Build] · Extends: Lab 7 · Time: 6–8 hours
The scenario¶
The catalogue now holds three kinds of item, and the circulation desk wants two policies
changed without a rebuild: journals lend for two days during term and overnight during
examinations, and the university is about to join a consortium whose items are lent under
their rules, not ours. A rule that lives in a loanDays() function cannot do either.
The head librarian has also asked a question the current design cannot answer honestly: a loan charge is currently computed from a base field that means something different for each kind, and there is a kind of item — the reference-only journal — for which the concept does not apply at all.
Your task is to make the item contract explicit, and to make the policy that varies replaceable at run time.
The deliverable includes a contract audit. Before you write the finished version, audit your interface: one row per function, and for each one, an implementation that could not honour it. Anything you cannot find an argument against is either a genuinely small interface or an interface you have not examined yet — say which, honestly.
What you start from¶
- Your Lab 7 solution:
BaseItemwith the three kinds, and the fleet-style container of handles that cannot slice. examples/module-08/01-shape-abstraction/main.cpp— the contract pattern, including a non-virtual public function that calls the pure virtuals.examples/module-08/04-abstract-base-and-interface/main.cpp— a stateful abstract base next to an interface, and the reason only one of them can be safely inherited twice.
Requirements¶
Itemis an interface or an abstract base, and it cannot be instantiated. State in a comment which of the two you chose and why: shared state and a partial algorithm, or a capability and nothing else.- No function that any kind cannot honour without lying. If a kind cannot support an
operation, it must not have it — express the capability elsewhere. (Interacts with
Module 4: an implementation that returns
falsefor an unsupported operation is a lie with a friendly signature.) - A
LoanPolicyabstraction with at least two implementations — the term-time policy and the examinations policy — injected into the item or into the loan calculation. The catalogue must be able to use a different policy without recompiling the item classes. (Interacts with Module 6: composition, not inheritance, is how a policy is exchanged at run time.) - One non-virtual public function on the item that uses the pure virtuals — the reporting line, or the charge calculation — so the base owns the algorithm and the subclasses supply the parts.
- A contract written in words for the charge operation: preconditions, postconditions and the invariant of the charge itself; and the same contract checked against two implementations, including one the contract turns out to need restating for.
- The contract audit, as a Markdown table, with the argument against each function you could not defend.
- Ownership remains explicit: the catalogue holds handles, and whatever it does not own is named in a comment. (Module 11 replaces the mechanism; the decision is yours here.)
- It must build with no warnings using the course's compile command:
g++ -std=c++17 -Wall -Wextra -Werror.
What to deliver¶
- A program that builds a catalogue of three item kinds, applies two different loan
policies to the same items, and prints loan periods and charges without a single type
check,
switchordynamic_cast. - The contract, for the charge operation, in four lines: precondition, postcondition on success, postcondition on refusal, invariant.
- The contract audit, one row per item-interface function.
- The design note, five sentences: what
Itemis (interface or abstract base), what is not in it and where that went, what the policy costs in complexity, what a fourth kind of item requires, and what would change your mind about the split.
Self-assessment checklist¶
It builds and runs
- [ ] Compiles with
g++ -std=c++17 -Wall -Wextra -Werrorwith no warnings - [ ]
Item item;fails to compile, and you have recorded the compiler's message - [ ] Both policies produce different, correct results on the same catalogue
- [ ] No
switch, type tag,dynamic_castortypeidcomparison in the catalogue's loops
It is correct
- [ ] No pure virtual function is implemented by a class that cannot honour it
- [ ] The charge operation's four contract lines match what the program actually does, for both policies, including the refusal path
- [ ] The reference-only item — the kind for which lending is meaningless — is modelled without a fabricated loan period
- [ ] Boundary cases work: an empty catalogue, a zero-day loan, a policy that refuses an item
- [ ] The failure path is exercised: an item that cannot be loaned under the current policy is refused and the reason is visible to the caller
It is designed
- [ ] I can state each type's responsibility in one sentence
- [ ] I chose interface or abstract base deliberately, and the design note says what the choice gives up
- [ ] Changing the policy does not require editing any item class
- [ ] The audit's "could not honour it" column has at least two entries, or an explicit argument for why the interface is small enough to have none
- [ ] Ownership is stated where the container is declared
It is verified
- [ ] I recorded the contract's failure path rather than reasoning that it works
- [ ] I implemented the second policy after the first was working, and can say what the first version got wrong
- [ ] The predictions in my trace were written before the run (Module 7's rule, kept here)
- [ ] The audit table was written before the final interface, not after it — and any row that changed the design is marked
Common ways this lab goes wrong¶
| Symptom | Cause | What to do |
|---|---|---|
error: cannot declare variable 'item' to be of abstract type 'Item' |
A pure virtual function is still un-overridden in the class you are instantiating | Read the note: lines; they name the function and the base declaration |
| The same error, but each kind compiles alone | One kind's override has a signature mismatch — usually a missing const |
Add override; the mismatch is then reported at its own line |
| A kind implements an operation that makes no sense for it | The interface is too wide | Move the capability out; the audit is the record of this decision |
Swapping the policy needs a rebuild, or a switch |
The policy is a compile-time constant or a tag, not an object | Inject a LoanPolicy reference or pointer into the calculation |
| Every policy change edits every item class | The policy was implemented in each subclass instead of in one object | One policy per concern; the item asks the policy, it does not answer for it |
A link error naming nothing, or undefined reference to 'Item::~Item()' |
A pure virtual destructor was declared and never defined | Define it, or use virtual ~Item() = default; |
| The contract says "the charge is correct" | The contract was written as a summary rather than as conditions | Rewrite it as: given this, then this; and state what happens when the precondition fails |
| The audit has no "could not honour it" entries and no argument | The audit was written after the interface, as justification | Rewrite it against a plausible fourth kind — an e-resource with an expiry — and see what falls out |
Reference solution¶
There is no published file for this lab, on purpose: it is your Lab 7 catalogue with its contract made explicit and its policy extracted, and the decisions are the work. What follows is the part the module's examples do not show together — an item that cannot be instantiated, a policy it does not own, and the audit that decides which functions belong where.
// The parts to compare your version against.
// ── the policy: a separate abstraction, because it changes without the items changing ──
class LoanPolicy {
public:
virtual ~LoanPolicy() = default;
virtual int loanDays(const Category category) const = 0; // the only question
virtual std::string name() const = 0;
};
class TermTimePolicy : public LoanPolicy {
public:
int loanDays(const Category category) const override { … }
std::string name() const override { return "term time"; }
};
class ExaminationPolicy : public LoanPolicy {
public:
int loanDays(const Category category) const override { … }
std::string name() const override { return "examinations"; }
};
// ── the item contract: no loan period of its own, no charge field, no shelf flag setter ──
class Item {
public:
virtual ~Item() = default;
virtual std::string id() const = 0;
virtual std::string title() const = 0;
virtual Category category() const = 0;
// Lending is a capability of *some* items, not all: a reference-only journal
// does not implement a fabricated period. Hence the policy is asked, and the
// item only reports what it is.
bool isLoanable() const { return loanable_; }
// Non-virtual, and correct for every kind because it calls the virtuals. The
// policy is supplied by the caller — the item does not own it.
std::string loanLine(const LoanPolicy& policy) const {
if (!isLoanable()) {
return id() + " " + title() + " reference only: not loaned";
}
return id() + " " + title() + " " + std::to_string(policy.loanDays(category())) +
" days under " + policy.name();
}
protected:
void setLoanable(bool loanable) { loanable_ = loanable; }
private:
bool loanable_ = true;
};
And the audit, written the way the lab requires — against implementations that do not exist yet, because that is where a wide interface reveals itself:
function could an implementation fail to honour it?
---------------------- ------------------------------------------------------
id() no. every item in the catalogue has an identifier.
title() no. every item has a title, even if it is a working one.
category() no. three kinds, each with a category.
isLoanable() no — but note: it is a bool, and a fourth kind could want
a reason. If two kinds ever need different reasons, this
becomes a pure virtual question with a richer answer.
loanDays() YES — deliberately not here. An e-resource with an
access expiry has no loan period at all, and a
reference-only journal has none either. It moved to
LoanPolicy, where it is a rule rather than a property.
Four things to look for when comparing with your version:
- Whether any function ended up as a refusal.
isLoanable()might have beenloanDays()returning0for "not loanable" — a lie with a friendly signature, and the exact defect this module exists to remove. Aboolquery with an honest name is better than a numeric answer that means "no". - Whether the policy is owned by the item. If
Itemholds aLoanPolicy&, every item shares one policy and an item cannot outlive it — a lifetime rule the design note must state. If the caller passes it toloanLine, ownership stays with the caller, which is usually the right answer and always the one that needs saying out loud. - Whether the contract's refusal path is real. "A policy that refuses an item" is a precondition or a postcondition in the contract, and a branch in the program. A contract whose failure case has never been run is a contract that has not been tested.
- Whether the audit changed anything. The best audits produce a change — a function that
moved out, a
boolthat became a question, a capability that split. An audit with no change is either a very good interface or an unexamined one, and the design note must say which it was.
Next: Challenge problems — three open problems, including an evaluation of the rule this whole module rests on. · All modules