Slotproject: een uitleenbibliotheek voor de buurt · oefening
Implementeer het belangrijkste object met gedrag
Dit is de methode waarvoor het hele project bestaat.
In checkout komen alle regels uit het contract samen: het item moet bestaan, het lid moet bestaan, het item moet beschikbaar zijn en het lid moet onder zijn leenlimiet zitten. Pas dan ontstaat een uitlening.
Eerst weigeren, als laatste wijzigen
De volgorde binnen de methode is niet willekeurig.
def checkout(self, item_id, member_id, checkout_day):
if item_id not in self._items:
raise UnknownItemError(...)
if member_id not in self._members:
raise UnknownMemberError(...)
if item_id in self._loans:
raise ItemUnavailableError(...)
...
# only now does anything change
Elke controle vindt plaats vóór er iets verandert. Het is de moeite waard om dat als regel te formuleren, want het alternatief veroorzaakt een fout die heel lastig te vinden is: een methode registreert de uitlening, ontdekt daarna dat het lid over zijn limiet zit en werpt een exceptie op. De aanroeper ziet een exceptie en neemt redelijkerwijs aan dat er niets is gebeurd. De bibliotheek denkt daar anders over.
Hoofdstuk 5 noemde dit samenhangende toestand gezamenlijk wijzigen en gaf vanuit de andere kant hetzelfde advies: een object mag nooit in een half gewijzigde toestand waarneembaar zijn. Eerst alle redenen om te weigeren controleren is de eenvoudigste manier om dat te bereiken.
Vier vragen, vier verschillende fouten
Het meegeleverde errors.py geeft je een kleine familie:
LibraryError
├── UnknownItemError
├── UnknownMemberError
├── ItemUnavailableError
└── LoanLimitReachedError
Hoofdstuk 12 pleitte precies voor deze vorm. Een aanroeper die alleen wil weten dát er iets misging, vangt LibraryError op. Een aanroeper die een gebruikersinterface bouwt, wil in het ene geval zeggen “die is al uitgeleend, probeer het volgende week” en in het andere “je hebt er al te veel geleend”. Dat kan alleen als de vier gevallen te onderscheiden zijn.
Weersta de verleiding om ze samen te voegen. “Kan niet uitlenen” is één melding voor vier verschillende situaties. Degene die de melding leest, moet raden welke van toepassing is.
Tellen wat een lid heeft geleend
Voor de leenlimiet heb je een aantal nodig en dat aantal wordt nergens opgeslagen. Dat is bewust: in les 2 hield je uitleningen buiten Member, zodat precies één plek ze kent.
Daarom telt de bibliotheek ze:
in_hand = 0
for loan in self._loans.values():
if loan.member_id == member_id:
in_hand = in_hand + 1
Door het aantal af te leiden in plaats van op te slaan, kan het niet verouderen. Een opgeslagen teller is een tweede bron van waarheid die bij elke uitlening en elke teruggave moet worden aangepast. Uiteindelijk vergeet één route dat.
Waarom tel je de uitleningen van een lid vanuit self._loans in plaats van op Member een totaal bij te houden?
De leentermijn, voorlopig
Een uitlening heeft een uiterste inleverdag nodig. Daarom moet checkout bepalen hoe lang het item geleend mag worden.
Gebruik voorlopig één leentermijn als constante bovenaan het bestand:
LOAN_PERIOD_DAYS = 14
Je weet al dat dit niet klopt. Een naslagwerk en een bordspel krijgen niet allebei dezelfde twee weken. In les 7 los je dat goed op. Eerst de eenvoudige versie schrijven is bewust: zo werkt de methode van begin tot eind en staat de beslissing op één duidelijke plek. Die later verplaatsen wordt dan een kleine, veilige wijziging in plaats van een zoektocht.
Een item terugbrengen
return_item(item_id) is het spiegelbeeld. Er is één reden om te weigeren, namelijk een item dat op dat moment niet is uitgeleend, en één ding om te doen: de uitlening verwijderen en teruggeven, zodat de aanroeper kan rapporteren wat er is teruggebracht.
Door de uitlening te verwijderen wordt het item weer beschikbaar. Er is geen aparte vlag voor “beschikbaar” en die hoort er ook niet te zijn: een vlag en een uitleenrecord zeggen op twee manieren hetzelfde. In les 6 zie je hoe je de beschikbaarheidsvraag alleen vanuit de uitleningen beantwoordt.
Opdracht
Implementeer checkout en return_item in library.py.
Voeg LOAN_PERIOD_DAYS = 14 bovenaan het bestand toe en importeer naast LibraryError de vier specifieke excepties.
checkout(item_id, member_id, checkout_day) weigert in deze volgorde, voordat er iets verandert:
een onbekende
item_idwerptUnknownItemError(item_id)opeen onbekende
member_idwerptUnknownMemberError(member_id)opeen item dat al is uitgeleend werpt
ItemUnavailableError(item_id, due_day)op, met gegevens uit de bestaande uitleningeen lid dat zijn
loan_limital heeft bereikt werptLoanLimitReachedError(member_id, limit)op, met de limiet van dat lid
Geef die waarden afzonderlijk door in plaats van hier een melding op te bouwen. De meegeleverde exceptieklassen accepteren ze nu al; in les 8 geef je ze benoemde attributen en leesbare meldingen. Importeer Loan uit models.
Anders registreert de methode een Loan die uiterlijk LOAN_PERIOD_DAYS na checkout_day terug moet zijn, slaat die op onder de id van het item en geeft die terug.
return_item(item_id) werpt LibraryError op wanneer dat item op dat moment niet is uitgeleend. Anders verwijdert de methode de uitlening en geeft die terug.
Tel de huidige uitleningen van een lid vanuit de opgeslagen uitleningen. Voeg geen teller toe aan Member.