Skip to content

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: BaseItem with 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

  1. Item is 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.
  2. 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 false for an unsupported operation is a lie with a friendly signature.)
  3. A LoanPolicy abstraction 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.)
  4. 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.
  5. 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.
  6. The contract audit, as a Markdown table, with the argument against each function you could not defend.
  7. 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.)
  8. 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, switch or dynamic_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 Item is (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 -Werror with 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_cast or typeid comparison 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 been loanDays() returning 0 for "not loanable" — a lie with a friendly signature, and the exact defect this module exists to remove. A bool query with an honest name is better than a numeric answer that means "no".
  • Whether the policy is owned by the item. If Item holds a LoanPolicy&, every item shares one policy and an item cannot outlive it — a lifetime rule the design note must state. If the caller passes it to loanLine, 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 bool that 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