Slotproject: een uitleenbibliotheek voor de buurt · oefening
Bewaak toestand en ontwerp excepties
De bibliotheek weigert de juiste dingen. Alleen de manier waarop is nog slecht.
Probeer een item uit te lenen dat al is uitgeleend en lees wat er terugkomt:
ItemUnavailableError: ('b1', 24)
Dat is het standaardgedrag van Python: zonder eigen constructor slaat een exceptie alles op wat ze meekrijgt en drukt de tuple af. Alle informatie is aanwezig, maar als een pakket losse onderdelen in plaats van een zin.
Een exceptie is een object dat je ontwerpt
Hoofdstuk 12 betoogde dit en hier levert het iets op. Een exceptie is geen etiket. Het is een object dat bij een aanroeper terechtkomt die er iets mee moet doen. Wat het bevat, is een ontwerpkeuze.
Twee doelgroepen hebben verschillende dingen nodig van dezelfde fout.
Een mens heeft een zin nodig: item ‘b1’ is on loan until day 24, oftewel: item ‘b1’ is uitgeleend tot dag 24. Die hoeft niet te weten in welke volgorde de argumenten stonden.
Een programma heeft de onderdelen nodig: de interface die zegt “probeer het na dag 24 opnieuw” heeft 24 als getal nodig, niet als deelstring die weer uit het Engels ontleed moet worden.
Een goed ontworpen exceptie bedient beide. Dat doe je met een constructor die de onderdelen krijgt, ze als attributen bewaart en zelf de zin opbouwt:
class ItemUnavailableError(LibraryError):
def __init__(self, item_id, due_day):
super().__init__(f"item {item_id!r} is on loan until day {due_day}")
self.item_id = item_id
self.due_day = due_day
super().__init__(...) zorgt ervoor dat str(error) de zin is. De twee toewijzingen maken de onderdelen bereikbaar. Geen van beide vervangt de andere.
Waarom de aanroepen niet veranderen
Kijk wat checkout al schrijft:
raise ItemUnavailableError(item_id, self._loans[item_id].due_day)
Die geeft de onderdelen al de hele tijd afzonderlijk door. De methode heeft nooit een melding opgebouwd en hoort dat ook niet te doen: een methode diep in de bibliotheek weet niet of de aanroeper een webpagina, een test of een terminal is. De formulering is dus niet haar verantwoordelijkheid.
Daarom bewerk je in deze les alleen errors.py. De informatie werd al doorgegeven; je geeft haar nu een plek.
Waarom sla je due_day als attribuut op als het al in de melding staat?
De familie en het doel van de basisklasse
LibraryError blijft precies zoals die is: een eenvoudige subklasse van Exception met een docstring en verder niets.
Dat is geen luiheid. De hele taak van die klasse is een naam te bieden die je kunt opvangen:
try:
library.checkout(item_id, member_id, today)
except LibraryError as error:
print(f"Sorry: {error}")
Eén handler, elke weigering, voor elk een zin. Een aanroeper die iets specifiekers wil zeggen, vangt eerst ItemUnavailableError op en leest error.due_day. De vervangbaarheidsregel uit hoofdstuk 9 maakt beide tegelijk mogelijk: elk van de vier is een LibraryError, dus de basisklasse opvangen kan er nooit een missen.
Later een vijfde soort weigering toevoegen betekent een klasse toevoegen. Je hoeft daarvoor niemands except-clausule opnieuw te bekijken.
Wat je nu bouwt
Geef elk van de vier een constructor die de onderdelen krijgt die library.py al doorgeeft, er een leesbare melding van maakt en de onderdelen als attributen bewaart.
Laat LibraryError met rust.
Opdracht
Geef de vier domeinexcepties echte constructors in errors.py.
Elke constructor krijgt de onderdelen die library.py al doorgeeft, bewaart ze als attributen met die namen en bouwt met super().__init__(...) zijn eigen melding:
UnknownItemError(item_id):"no item with id 'b1'", attribuutitem_id.UnknownMemberError(member_id):"no member with id 'm1'", attribuutmember_id.ItemUnavailableError(item_id, due_day):"item 'b1' is on loan until day 24", attributenitem_idendue_day.LoanLimitReachedError(member_id, limit):"member 'm1' already holds 2 item(s)", attributenmember_idenlimit.
Gebruik !r voor de id’s, zodat de aanhalingstekens duidelijk maken waar een id begint en eindigt.
Laat LibraryError precies zoals die is. Die biedt één naam waarmee je alle vier opvangt.