Abschlussprojekt: Eine gemeinschaftliche Leihbibliothek · Übung
Begriffe und Verantwortlichkeiten erkennen
Die Schnittstellenzusage sagt dir, was Library tun muss. Sie sagt dir nicht, was sonst noch existieren muss. Das ist die Entwurfsarbeit.
Die Substantive sind ein erster Entwurf, nicht die Antwort
Lies die Zusage erneut und sammle die Substantive: Mitglied, Gegenstand, Buch, Spiel, Ausleihe, Bibliothek, Katalog, Tag. Mit einer solchen Liste beginnen die meisten Entwürfe. Sie ist hilfreich, aber betrachte sie als ersten Entwurf. Einige Begriffe werden Klassen, andere Attribute. Manche entpuppen sich als dieselbe Sache unter zwei Namen.
Drei Fragen lohnen sich für jeden Kandidaten.
Hat er Zustand, der geschützt werden sollte? Ein Mitglied hat einen Namen und eine Ausleihgrenze, die sinnvoll bleiben müssen. Eine Tagesnummer nicht; sie ist nur eine Ganzzahl.
Stellt ihm etwas Fragen? Ein Buch wird gefragt, wie lange es ausgeliehen werden darf. Der Katalog wird gefragt, was verfügbar ist. Wenn niemand einen Kandidaten je etwas fragt, sind es wahrscheinlich Daten, die zu etwas anderem gehören.
Wären zwei davon unterschiedlich? Zwei Mitglieder sind sinnvoll voneinander verschieden. Hier gibt es immer nur eine Bibliothek. „Der Katalog“ existiert nicht unabhängig von der Bibliothek, die ihn verwaltet. Deshalb ist er eine Sammlung innerhalb von Library statt eine eigene Klasse.
Vier bleiben übrig: Book, Game, Member und Loan. Diese Lektion baut die ersten drei. Loan wartet bis Lektion 4, weil es eine andere Art von Objekt ist und eigenen Raum verdient.
Verantwortlichkeit ist, was ein Objekt entscheidet
Kapitel 6 formulierte es so: Die Verantwortlichkeit eines Objekts besteht aus den Entscheidungen, die es selbstständig trifft.
Member entscheidet, ob ein Name und eine Ausleihgrenze akzeptabel sind. Nicht Library und nicht der Code, der ein Mitglied erstellt. Läge die Validierung in Library.register_member, würde jeder andere Weg zu einem Member sie überspringen. Es gäbe dann kein Mitglied, dem du vertrauen kannst.
Das ist die Regel aus Kapitel 5 in einem größeren Programm: Ein Konstruktor soll es ablehnen, etwas Ungültiges zu bauen.
class Member:
def __init__(self, member_id, name, loan_limit):
if not name.strip():
raise ValueError("a member needs a name")
...
Beachte, was hier nicht vorkommt. Ein Mitglied weiß nicht, welche Gegenstände es ausgeliehen hat. Das ist eine Tatsache über Ausleihen. Sie auf Member zu speichern würde bedeuten, dass zwei Stellen über dieselbe Sache übereinstimmen müssen. Kapitel 6 warnte davor: Wenn zwei Objekte dieselbe Beziehung verwalten, werden sie auseinanderlaufen.
Warum lehnt Member einen leeren Namen in seinem eigenen Konstruktor ab, statt dass Library.register_member ihn ablehnt?
Identität ist nicht dasselbe wie Gleichheit
Jeder Gegenstand hat eine item_id, jedes Mitglied eine member_id, denn die Operationen der Schnittstelle erhalten Kennungen statt Objekte.
Das ist nicht dieselbe Wertgleichheit wie in Kapitel 7. Book ist hier kein Wertobjekt: Zwei Exemplare desselben Buchs im Regal sind zwei verschiedene ausleihbare Dinge, auch wenn Titel und Autor übereinstimmen. Ihre Gegenstands-IDs müssen verschieden sein, wenn beide Exemplare im Katalog stehen. Lass __eq__ deshalb unverändert. Identität ist genau das Gewünschte, und über die ID findet die Bibliothek ein bestimmtes Exemplar.
Was Book und Game enthalten
Beide brauchen eine item_id und einen title, denn das benötigen die Bibliothek und die Berichte von jedem Gegenstand.
Darüber hinaus unterscheiden sie sich. Genau dafür gibt es zwei Klassen. Ein Buch hat einen author. Ein Spiel hat eine Spielerzahl players.
Beide beantworten außerdem zwei Fragen, auf die sich der Rest des Programms stützt:
class Book:
def loan_period_days(self):
return 21
def describe(self):
return f"{self.title} by {self.author}"
Dieselben zwei Namen auf Game, aber mit anderen Antworten: eine Woche statt drei und ein Satz über Spieler statt über einen Autor.
Diese Methoden hier statt in Library zu platzieren folgt wieder der Verantwortlichkeitsregel. Wie lange ein Spiel behalten werden darf, ist eine Tatsache über Spiele. Eine Bibliothek, die das selbst entscheidet, müsste jede jemals hinzukommende Art von Gegenstand kennen. In Lektion 7 wird der Unterschied zwischen diesen beiden Entwürfen konkret.
Aufgabe
Schreibe die drei Klassen für die Begriffe der Domäne in models.py.
Book(item_id, title, author) und Game(item_id, title, players) speichern jeweils ihre Argumente als gleichnamige Attribute. Keine der beiden Klassen validiert bisher etwas.
Beide beantworten außerdem die zwei Fragen, die jeder ausleihbare Gegenstand beantwortet:
loan_period_days()gibt bei einem Buch21zurück, bei einem Spiel7.describe()gibt entsprechend"Solaris by Lem"und"Carcassonne (up to 5 players)"zurück.
Member(member_id, name, loan_limit) speichert seine drei Werte und lehnt die Konstruktion ab, wenn eine dieser Regeln verletzt wird:
nameist leer oder besteht nur aus Leerraum, oderloan_limitist kleiner als 1.
Beide Ablehnungen lösen ValueError mit einer Meldung aus, die das Problem beschreibt.
Halte die Validierung innerhalb von Member.__init__. Nichts anderes im Projekt soll ein Mitglied erzeugen können, das diese Regeln verletzt.