Skip to content

Practice Lab 4 — Bank account as a type

Difficulty: Beginner · Estimated time: 3–4 hours · Module 2: Classes and Objects

Two objects in one operation is where a class stops being a container for fields and becomes a model of something. Transfers are also where the first genuinely dangerous invariant appears.


At a glance

Difficulty Beginner
Estimated time 3–4 hours
Module Module 2 — Classes and Objects
Concepts practised Classes with state and behaviour, const operations, object interaction, passing objects by reference, invariants between two objects
Prerequisite Practice Lab 3
Class design given at the level of responsibilities; you choose the operations' signatures

Scenario

A credit union's smallest branch keeps two kinds of account for its members: a current account, which may go overdrawn by arrangement, and a savings account, which may not. Members move money between their own accounts, and the branch needs a daily summary.

The branch manager has one rule she is firm about: an account's balance must never be changed by anything except a recorded transaction. She has been burned before by a program that quietly adjusted a balance.


Problem statement

Implement a small account model with:

  1. Account — an identifier, a holder's name, a balance in whole pence, and whether an overdraft is permitted.
  2. Operations: deposit, withdraw, balance, describe, and an operation that reports whether a given amount could be withdrawn now.
  3. Transaction — a record of one movement: which account, the amount, the direction, and the balance after it. Immutable once created.
  4. A ledger — the sequence of transactions, with operations to append one and to print them.
  5. A transfer operation that moves money between two accounts by appending two transactions, and which must not leave one side applied if the other is refused.

Refusals must name the amount and the reason: insufficient funds, overdraft not permitted, a non-positive amount.


Learning objectives

By the end of this lab you will be able to:

  • Design a class whose state can only be changed through its own operations.
  • Distinguish an operation that queries an object (const) from one that changes it.
  • Write an operation that takes two objects and keeps an invariant that spans both of them.
  • Explain why a transfer that applies one side and refuses the other is worse than refusing the whole operation.
  • State your balance invariant in one sentence, and say where it is enforced.

Functional requirements

  1. An account is created with an identifier, a holder, and whether an overdraft is permitted; the balance starts at zero.
  2. A deposit of a non-positive amount is refused.
  3. A withdrawal is refused when it would take the balance below zero with no overdraft, or below the agreed overdraft limit when one exists.
  4. Every accepted deposit or withdrawal appends exactly one transaction, whose recorded balance equals the account's balance afterwards.
  5. A transfer appends two transactions — one per account — and the sum of their amounts is zero.
  6. A transfer that is refused leaves both accounts unchanged and appends nothing.
  7. The ledger prints in order, one line per transaction, and its last line's balance equals the current balance of the account it names.
  8. describe prints identifier, holder, balance and overdraft status on one line.
  9. Money is a whole number of pence everywhere, and printing divides only at the edge.

Constraints

  • Two source files are allowed and encouraged: a header for the classes and a .cpp for their operations, with main.cpp including the header. Compile with: g++ -std=c++17 -Wall -Wextra -Werror account.cpp main.cpp -o branch
  • No public data members and no setter for the balance. If a caller can set a balance, rule 4 is unenforceable.
  • Every query is const, and you must be able to print a const Account in main.
  • No inheritance, no templates, no smart pointers yet.
  • Integer pence; no double anywhere.
  • No warnings.

Starter task

Answer these in design-notes.md before writing code:

  1. Where does the overdraft rule live, and how many places call it? (One, if your design is right.)
  2. Transaction records a balance after the movement. Which object decides that value, and what would go wrong if the caller supplied it?
  3. Write the transfer operation's signature, and say what it returns. How does a caller learn that the transfer was refused, and what does it learn about why?
  4. A transfer needs to change two Account objects. Which class owns the transfer, or is it a free function over two accounts? Justify your choice in two sentences.

Implementation tasks

  1. Implement Account with its refusing operations, and make the refusal messages name the amount.
  2. Implement Transaction and the ledger, with the append operation as the only way in.
  3. Implement the transfer, and make it refuse as a whole rather than in halves.
  4. Write main: open two accounts, transfer between them, attempt an illegal withdrawal, print both accounts and the ledger.
  5. Run every test case and record the output. Include the ledger print after the refused transfer.

Test cases

# Action Expected result
1 Deposit 5000 into a new account Accepted; balance 5000; one transaction
2 Deposit −100 Refused, naming the amount
3 Withdraw 6000 from a savings account with 5000 Refused, naming the shortfall
4 Withdraw 6000 from a current account with a 2000 overdraft Accepted; balance −1000
5 Withdraw 8000 from that same account Refused; balance still −1000
6 Transfer 2000 from A to B Two transactions; A's balance down 2000, B's up 2000
7 Transfer 100000 from A to B Refused as a whole; both balances unchanged; no new transactions
8 Print the ledger after case 7 The same number of lines as before it
9 Print a const Account in main Compiles
10 Ledger's last line versus balance() Equal for each account

Boundary cases to run deliberately: deposit of 1; withdrawal of exactly the balance; withdrawal taking the balance to exactly the overdraft limit; a transfer of the entire balance.


Expected behaviour

$ ./branch
A-1001  Ayesha Khan   balance 0 pence        (current, overdraft 2000)
A-1002  Bilal Ahmed   balance 0 pence        (savings, no overdraft)
deposit A-1001 5000 -> accepted, balance 5000
withdraw A-1002 6000 -> refused: insufficient funds (short by 6000 pence)
withdraw A-1001 6000 -> accepted, balance -1000
transfer A-1001 -> A-1002 2000 -> refused: insufficient funds on A-1001
ledger:
  #1  A-1001  +5000  balance 5000
  #2  A-1001  -6000  balance -1000
  (the refused transfer appended nothing)

Extension challenge

Add a credit limit per account and a daily withdrawal total, so that a third consecutive withdrawal of the day is refused even when funds allow.

Then record, in design-notes.md:

  • Which operations had to consult the new rule, and how many call sites there now are.
  • Whether the rule belongs on the account, on the ledger, or on a separate object — and what each choice would cost a caller.
  • The change your ledger needs so a daily total can be computed without scanning every transaction.

Debugging challenge

This transfer is reported as losing money:

bool transfer(Account& from, Account& to, int amount) {
    if (!from.withdraw(amount)) {
        return false;
    }
    to.deposit(amount + 1);   // "round the transfer up in the recipient's favour"
    return true;
}

Reproduce it — one transfer, then print both balances and the sum — and answer:

  1. What is the invariant a transfer must preserve, stated as an equation?
  2. Which line breaks it, and why would no compiler warning catch this?
  3. Where in the design should that equation have been checkable, and what would you put there?

Reflection questions

  1. Your withdraw returns something. What does it return, and is "false" enough for a caller to tell a shortfall from an unpermitted overdraft?
  2. Transaction is immutable after construction. What does that buy, and what does it cost when you need to correct a mistake?
  3. Where is your balance invariant enforced? Try to break it from main and record what stopped you.
  4. A caller wants to know "can I withdraw 500?" without doing it. Which operation answers that, and what is the risk of a caller using the answer and then withdrawing anyway?
  5. Two accounts are involved in a transfer. Which object owns the operation in your design, and what would change if ownership moved?
  6. If the ledger and the balance ever disagreed, which would you believe, and what does that say about your design?

Completion checklist

  • [ ] It compiles from a documented command with -Wall -Wextra -Werror and no warnings
  • [ ] No public data members, and no way to set a balance directly
  • [ ] Every query is const, proved by the const Account in main
  • [ ] Every accepted movement appends exactly one transaction
  • [ ] A refused transfer leaves both accounts and the ledger untouched — output pasted
  • [ ] All ten test cases run, with output recorded
  • [ ] Boundary cases run, including a withdrawal of exactly the balance
  • [ ] design-notes.md states the overdraft rule's single home and the transfer's ownership decision
  • [ ] The debugging challenge was reproduced and explained before being fixed
  • [ ] I can state the transfer invariant as an equation, and say where it is enforced

Related: Lab index · Module 2 — Classes and Objects · Previous: Practice Lab 3 · Next: Practice Lab 5 — Employee record with a lifecycle trace