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
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
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 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
Both also answer the two questions every lendable item answers:
loan_period_days()returns21on a book and7on 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:
nameis empty or only whitespace, orloan_limitis less than 1.
Both refusals raise
Keep the validation inside Member.__init__. Nothing else in the project should be able to produce a member that breaks those rules.