0%

Hoofdstuk 14 · oefening

Slotproject: een uitleenbibliotheek voor de buurt

Kies het probleem en lees het contract

Dertien hoofdstukken hebben je de onderdelen aangereikt. In dit hoofdstuk bouw je iets waarin je ze allemaal tegelijk gebruikt.

Je schrijft een uitleenbibliotheek voor de buurt: die registreert leden, houdt een catalogus bij van spullen die je kunt lenen, leent ze uit, neemt ze terug, vertelt wat beschikbaar is en zoekt uit welke uitleningen te laat zijn. Geen web, geen database, geen bestanden en geen datums behalve een gewoon dagnummer. Alles wat dit interessant maakt, draait om het ontwerp.

Dit hoofdstuk begint met een nieuw project. Je bestanden blijven binnen dit hoofdstuk van les tot les bewaard; bewerk het genoemde bestand in de projectboom. Run voert altijd main.py uit, dat tot de laatste les een tijdelijke invulling blijft. Submit controleert de huidige mijlpaal in het hele project. Reset herstelt de beginbestanden van het hoofdstuk en verwijdert je werk, terwijl de voltooiingsvinkjes blijven staan.

Wat een contract is

Je hoeft de interface niet te bedenken. Die is gegeven en heet een contract, omdat het de belofte is waarop de rest van de wereld mag vertrouwen.

register_member(member)
add_item(item)
checkout(item_id, member_id, checkout_day)
return_item(item_id)
available_items()
active_loans()
overdue_loans(on_day)

Zeven bewerkingen op één klasse, Library. Alles in het project staat in dienst van die zeven.

Zo komt echt werk meestal binnen. Iemand anders heeft besloten wat het moet doen, soms omdat andere code het al aanroept, en jij bouwt iets dat die beloften nakomt. Je hebt wel degelijk vrijheid, maar die zit onder het contract: hoe je items opslaat, hoe je hulpfuncties noemt en of Book en Game een basisklasse delen. Dat is aan niemand beloofd en ligt dus niet vast.

Begin met de vorm, niet met het gedrag

De eerste stap bij een project van deze omvang is niet de lastigste methode schrijven. Je begint door de vorm neer te zetten.

class Library:
    def register_member(self, member):
        raise NotImplementedError

NotImplementedError is Pythons manier om te zeggen: dit hoort te bestaan, maar werkt nog niet. Het is geen excuus; het is een tijdelijke invulling die duidelijk faalt. Als iets die per ongeluk aanroept, merk je dat meteen in plaats van ongemerkt een None te krijgen.

Een bestand vol van zulke tijdelijke invullingen is meer waard dan het lijkt. Het geeft je een controlelijst die je kunt uitvoeren, dwingt je over de parameters van elke methode na te denken voordat de inhoud je afleidt en zorgt ervoor dat je in elke volgende les iets invult in plaats van structuur te bedenken.

Waarom schrijf je raise NotImplementedError in plaats van pass als methode-inhoud te laten staan?

Een signatuur zorgvuldig lezen

Twee details in het contract lees je makkelijk over het hoofd. Allebei zijn het bewuste keuzes.

checkout(item_id, member_id, checkout_day) krijgt identificatoren, geen objecten. Een aanroeper die de id van een item heeft, hoeft het item zelf niet bij de hand te hebben. Zo houdt de bibliotheek de controle over haar eigen catalogus en hoeft ze niet te vertrouwen op een willekeurig aangeleverd object.

overdue_loans(on_day) krijgt een dag in plaats van een klok uit te lezen. Hoofdstuk 6 legde dit uit: een functie die de buitenwereld om de tijd vraagt, is veel moeilijker te testen dan een functie die de tijd meekrijgt. Om dezelfde reden is elke datum in dit project een gewoon geheel dagnummer.

Wat je nu bouwt

Maak Library in library.py, met alle zeven methoden, die elk NotImplementedError opwerpen.

Neem de parameternamen precies over. Latere lessen en de controles gebruiken ze.

Opdracht

Schrijf de publieke vorm van Library in library.py.

Definieer één klasse, Library, met een __init__ die alleen self krijgt, en deze zeven methoden:

  • register_member(self, member)

  • add_item(self, item)

  • checkout(self, item_id, member_id, checkout_day)

  • return_item(self, item_id)

  • available_items(self)

  • active_loans(self)

  • overdue_loans(self, on_day)

Elke methode bevat raise NotImplementedError. Er werkt nog niets en niets hoort te doen alsof.

Gebruik precies deze namen, in deze volgorde, met deze parameternamen. De rest van het hoofdstuk bouwt erop voort.