0%

Final Capstone: A Community Lending Library · practice

Identify Concepts and Responsibilities

The contract told you what Library must do. It did not tell you what else has to exist, and that is the design work.

The nouns are a first draft, not the answer

Read the contract again and collect the nouns: member, item, book, game, loan, library, catalog, day. That is where most designs start, and it is genuinely useful, but treat it as a first draft. Some of those become classes. Some are attributes. Some turn out to be the same thing wearing two names.

Three tests are worth applying to each candidate.

Does it have state worth protecting? A member has a name and a loan limit that must stay sensible. A day number does not; it is just an integer.

Does anything ask it questions? A book gets asked how long it can be borrowed for. The catalog gets asked what is available. If nothing ever asks a candidate anything, it is probably data hanging off something else.

Would two of them be different? Two members are meaningfully different from each other. There is only ever one library here, and “the catalog” is not a thing that exists apart from the library that owns it, so it is a collection inside Library rather than a class.

That leaves four: Book, Game, Member, and Loan. This lesson builds the first three. Loan waits for Lesson 4, because it is a different kind of and deserves the space.

Responsibility is what an object decides

Chapter 6 put it this way: an object’s responsibility is the set of decisions it makes on its own.

Member decides whether a name and a loan limit are acceptable. Not Library, and not whatever code constructs a member. If validation lived in Library.register_member, then every other route to a Member would skip it, and there would be no such thing as a member you can trust.

That is Chapter 5’s rule arriving in a bigger program: a constructor’s job is to refuse to build something invalid.

class Member:
    def __init__(self, member_id, name, loan_limit):
        if not name.strip():
            raise ValueError("a member needs a name")
        ...

Note what is not here. A member does not know which items it has borrowed. That is a fact about loans, and putting it on Member would mean two places have to agree about the same thing. Chapter 6 called that out: when two objects both track a relationship, they will drift.

Why does Member reject an empty name in its own constructor, rather than Library.register_member rejecting it?

Identity is not the same as equality

Every item carries an item_id, and every member a member_id, because the contract’s operations take identifiers rather than objects.

That is not the same as the equality from Chapter 7. Book here is not a value object: two copies of the same book on the shelf are two different things to lend, even when their title and author match. Their item ids must differ if both copies are in the catalog. So leave __eq__ alone. Identity is exactly what you want, and the id is how the library finds one.

What Book and Game hold

Both need an item_id and a title, because that is what the library and the reports need from any item.

Beyond that they differ, which is the point of having two classes. A book has an author. A game has a number of players.

Both also answer two questions, and these are the ones the rest of the program leans on:

class Book:
    def loan_period_days(self):
        return 21

    def describe(self):
        return f"{self.title} by {self.author}"

The same two names on Game, with different answers: one week rather than three, and a sentence about players rather than an author.

Putting them here rather than in Library is the responsibility rule once more. How long a game may be kept is a fact about games. A library that decided it would have to be told about every kind of item that ever exists, and Lesson 7 is where the difference between those two designs turns concrete.

Task

Write the three vocabulary classes in models.py.

Book(item_id, title, author) and Game(item_id, title, players) each store their as attributes of the same names. Neither validates anything yet.

Both also answer the two questions every lendable item answers:

  • loan_period_days() returns 21 on a book and 7 on a game.

  • describe() returns "Solaris by Lem" and "Carcassonne (up to 5 players)" respectively.

Member(member_id, name, loan_limit) stores its three, and refuses to be built when either rule is broken:

  • name is empty or only whitespace, or

  • loan_limit is less than 1.

Both refusals raise with a message saying what was wrong.

Keep the validation inside Member.__init__. Nothing else in the project should be able to produce a member that breaks those rules.