Final Capstone: A Community Lending Library · practice
Implement the Loan Value Object
Book, Game, and Member are all things with an identity. Two members named Mina are two people, and the library tells them apart by id.
A loan is not like that.
What makes a loan a value
A loan is a record: this item, to this member, from this day, due back on that day. Two loans with all four the same are not two loans. They are the same fact written twice.
Chapter 8 had a name for Book.
from dataclasses import dataclass
@dataclass(frozen=True)
class Loan:
item_id: str
member_id: str
checkout_day: int
due_day: int
frozen=True is doing real work. A loan that could be edited in place would let any code holding one quietly change the due date, and the library would never know. Frozen means the only way to change a loan is to replace it, which is a thing the library does deliberately.
You get __init__, a readable __repr__, and value equality for free. All three matter later: the repr shows up when a test fails, and the equality is what lets a test say assert loan in library.active_loans().
Storing ids, not objects
Look at what the loan holds: item_id and member_id, not Book and Member.
That is a deliberate choice and worth understanding, because the other version is tempting. Holding the objects would let you write loan.item.title instead of looking the item up.
Two reasons not to. The library already owns the catalog, so a loan holding the item would be a second place the same relationship lives, and Chapter 6 warned about exactly that. And a frozen loan holding a Member would only be shallowly frozen: the loan could not be reassigned, but the member inside it could change under you. A loan of ids is genuinely
The cost is real: to report a loan’s title you have to ask the library. That is the right direction for the dependency to point.
Loan is frozen=True. What would still be mutable if it held a Member object instead of a member_id?
One small piece of behavior
A value object is allowed to answer questions about itself. Chapter 8 made the point that a value object holding data and nothing else is often a missed opportunity.
is_overdue(on_day) is the natural one here:
def is_overdue(self, on_day):
return on_day > self.due_day
Notice it takes the day rather than reading a clock, the same way overdue_loans(on_day) does. And notice where the rule lives: whether a loan is late is a fact about the loan, so the comparison belongs on Loan rather than being written out again in every place that needs it.
Being late is >, not >=. An item due on day 20 is not overdue on day 20; it is overdue on day 21. That is one keystroke and it is the kind of decision worth pinning down with a test at the boundary, exactly as Chapter 13’s Lesson 5 argued.
Task
Add Loan to models.py, leaving Book, Game, and Member as they are.
Make it a frozen dataclass with four fields, in this order: item_id, member_id, checkout_day, due_day. The first two are
Give it one is_overdue(on_day), returning whether the loan is late on that day. A loan due on day 20 is not overdue on day 20.
Store ids, not Book or Member