Slotproject: een uitleenbibliotheek voor de buurt · oefening
Ondersteun polymorf gedrag
In les 5 liet je bewust iets onjuists staan:
LOAN_PERIOD_DAYS = 14
Elk item krijgt twee weken. Een roman en een bordspel zijn niet hetzelfde soort uitleenbaar voorwerp, en elke echte bibliotheek weet dat. Nu los je dit op. De manier waarop je dat doet, is de kern van deze les.
De versie die je afwijst
Hier is de voor de hand liggende reparatie. Het is nuttig die uit te schrijven, zodat je ziet wat eraan mankeert:
if isinstance(item, Book):
period = 21
elif isinstance(item, Game):
period = 7
Het werkt. Het zet ook een gegeven over boeken in Library. Bovendien moet je voor een derde type item checkout bewerken in plaats van alleen een nieuwe klasse toe te voegen.
Hoofdstuk 9 benoemde het waarschuwingssignaal: een reeks typecontroles betekent meestal dat een object vraagt “wat ben jij?” terwijl het het andere object iets zou moeten laten doen. De kennis over hoe lang een spel geleend mag worden, hoort bij Game, want dat is een gegeven over spellen.
Vraag het aan het item
class Book:
def loan_period_days(self):
return 21
class Game:
def loan_period_days(self):
return 7
En checkout neemt de beslissing niet meer zelf:
item = self._items[item_id]
due_day = checkout_day + item.loan_period_days()
Eén regel en alle typecontroles zijn weg. Library weet niet meer dat er zoiets als een boek bestaat. Ze weet dat ze items bevat en dat een item kan vertellen hoe lang het geleend mag worden.
Voeg volgend jaar een derde type item toe en checkout verandert niet. Dat is de eigenschap die je wilt hebben en die de versie met isinstance je niet kan geven.
Geen basisklasse nodig
Hoofdstuk 9 was hier zorgvuldig over, dus deze les is dat ook.
Book en Game delen geen ouderklasse. Ze zijn helemaal niet via overerving verwant. Ze zijn hier uitwisselbaar doordat ze allebei loan_period_days() en describe() beantwoorden en Library nooit iets anders vraagt.
Dat is duck typing en dat is genoeg. Een gedeelde basisklasse zou alleen iets opleveren als er echt gedrag te delen was. Dat is er nu niet: de twee implementaties hebben alleen hun naam gemeen.
Het contract is het daarmee eens en zegt dat ook:
Overerving is optioneel.
BookenGamemogen een kleine basisklasse delen of simpelweg via duck typing dezelfde publieke bewerkingen ondersteunen.
Kies de versie met minder constructies totdat extra constructies hun plek verdienen.
Book en Game delen geen basisklasse, maar Library behandelt ze hetzelfde. Waardoor werkt dat?
De tweede polymorfe methode
describe() is de andere bewerking die volgens het contract elk item moet ondersteunen. Hier wordt het verschil tussen de twee typen zichtbaar voor een mens in plaats van voor de code:
Solaris by Lem
Carcassonne (up to 5 players)
Dezelfde aanroep, een andere zin, en geen enkele aanroeper hoeft te weten welk type item die heeft. Daardoor kun je überhaupt een rapport over een gemengde catalogus schrijven.
Wat je nu bouwt
In les 2 kregen Book en Game beide bewerkingen al. Deze les gaat over de andere kant van die afspraak: zorgen dat Library ze ook echt gebruikt.
Je voegt describe_catalog() toe aan je opgeslagen klasse Library. Die rapporteert de beschikbare items in catalogusvolgorde door elk item om zijn beschrijving te vragen. De alternatieve startcode toont ter vergelijking een versie met isinstance, maar bij het openen van deze les blijft je opgeslagen bestand behouden; de nieuwe methode wordt er niet in gezet. Ook checkout moet veranderen: die past nu één getal op alles toe.
Beide worden één aanroep naar het item. Als je klaar bent, importeert library.py de klassen Book en Game niet meer en verwijst het er ook niet naar. Dat is wat je moet controleren.
Opdracht
Haal de typecontroles uit library.py.
Voeg describe_catalog(self) toe aan je bestaande klasse Library. Die geeft beschrijvingen terug van de items uit available_items(), in dezelfde volgorde, door voor elk item describe() aan te roepen. Gebruik je de alternatieve startcode, vervang dan de versie met typecontroles. Het openen van deze les overschrijft je opgeslagen bestand niet en voegt er geen methode aan toe.
Doe daarna hetzelfde met checkout. Die geeft nu alles LOAN_PERIOD_DAYS. Laat de methode de uiterste inleverdag berekenen door het item naar zijn eigen leentermijn te vragen en verwijder de constante.
Als je klaar bent, hoort library.py in uitvoerbare code Book of Game niet te importeren of ernaar te verwijzen, en ook geen itemtypen te onderzoeken. Commentaar over de refactoring mag wel. De bibliotheek bevat items en stelt ze vragen; welke soorten er bestaan gaat haar niet aan.