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:
Account— an identifier, a holder's name, a balance in whole pence, and whether an overdraft is permitted.- Operations:
deposit,withdraw,balance,describe, and an operation that reports whether a given amount could be withdrawn now. Transaction— a record of one movement: which account, the amount, the direction, and the balance after it. Immutable once created.- A ledger — the sequence of transactions, with operations to append one and to print them.
- 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¶
- An account is created with an identifier, a holder, and whether an overdraft is permitted; the balance starts at zero.
- A deposit of a non-positive amount is refused.
- A withdrawal is refused when it would take the balance below zero with no overdraft, or below the agreed overdraft limit when one exists.
- Every accepted deposit or withdrawal appends exactly one transaction, whose recorded balance equals the account's balance afterwards.
- A transfer appends two transactions — one per account — and the sum of their amounts is zero.
- A transfer that is refused leaves both accounts unchanged and appends nothing.
- The ledger prints in order, one line per transaction, and its last line's balance equals the current balance of the account it names.
describeprints identifier, holder, balance and overdraft status on one line.- 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
.cppfor their operations, withmain.cppincluding 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 aconst Accountinmain. - No inheritance, no templates, no smart pointers yet.
- Integer pence; no
doubleanywhere. - No warnings.
Starter task¶
Answer these in design-notes.md before writing code:
- Where does the overdraft rule live, and how many places call it? (One, if your design is right.)
Transactionrecords a balance after the movement. Which object decides that value, and what would go wrong if the caller supplied it?- 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?
- A transfer needs to change two
Accountobjects. Which class owns the transfer, or is it a free function over two accounts? Justify your choice in two sentences.
Implementation tasks¶
- Implement
Accountwith its refusing operations, and make the refusal messages name the amount. - Implement
Transactionand the ledger, with the append operation as the only way in. - Implement the transfer, and make it refuse as a whole rather than in halves.
- Write
main: open two accounts, transfer between them, attempt an illegal withdrawal, print both accounts and the ledger. - 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:
- What is the invariant a transfer must preserve, stated as an equation?
- Which line breaks it, and why would no compiler warning catch this?
- Where in the design should that equation have been checkable, and what would you put there?
Reflection questions¶
- Your
withdrawreturns something. What does it return, and is "false" enough for a caller to tell a shortfall from an unpermitted overdraft? Transactionis immutable after construction. What does that buy, and what does it cost when you need to correct a mistake?- Where is your balance invariant enforced? Try to break it from
mainand record what stopped you. - 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?
- Two accounts are involved in a transfer. Which object owns the operation in your design, and what would change if ownership moved?
- 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 -Werrorand no warnings - [ ] No public data members, and no way to set a balance directly
- [ ] Every query is
const, proved by theconst Accountinmain - [ ] 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.mdstates 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