0%

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 like that: objects. They are defined entirely by what they hold, they compare by their contents, and they do not change after they are made. That is exactly a loan, and it is why the contract calls for a dataclass here and not 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 all the way down.

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 , the last two are plain integer day numbers.

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 . The library owns those, and a frozen loan holding a object would not really be frozen.