0%

Slotproject: een uitleenbibliotheek voor de buurt · oefening

Bepaal begrippen en verantwoordelijkheden

Het contract vertelde wat Library moet doen. Het vertelde niet wat er verder moet bestaan. Dat is het ontwerpwerk.

De zelfstandige naamwoorden zijn een eerste opzet, niet het antwoord

Lees het contract opnieuw en verzamel de zelfstandige naamwoorden: lid, item, boek, spel, uitlening, bibliotheek, catalogus, dag. De meeste ontwerpen beginnen met zo’n lijst. Die is nuttig, maar behandel hem als een eerste opzet. Sommige woorden worden klassen. Andere worden attributen. Weer andere blijken twee namen voor hetzelfde te zijn.

Stel over elke kandidaat drie vragen.

Heeft het toestand die bescherming nodig heeft? Een lid heeft een naam en een leenlimiet die geldig moeten blijven. Een dagnummer niet; dat is gewoon een geheel getal.

Stelt iets er vragen aan? Een boek krijgt de vraag hoe lang het geleend mag worden. De catalogus krijgt de vraag wat beschikbaar is. Als niets ooit iets aan een kandidaat vraagt, gaat het waarschijnlijk om gegevens die bij iets anders horen.

Zouden twee exemplaren van elkaar verschillen? Twee leden verschillen wezenlijk van elkaar. Hier is er altijd maar één bibliotheek. “De catalogus” bestaat niet los van de bibliotheek die hem beheert, dus die wordt een collectie binnen Library en geen klasse.

Er blijven er vier over: Book, Game, Member en Loan. In deze les bouw je de eerste drie. Loan wacht tot les 4, omdat het een ander soort object is dat een eigen uitleg verdient.

Verantwoordelijkheid is wat een object beslist

Hoofdstuk 6 formuleerde het zo: de verantwoordelijkheid van een object bestaat uit de beslissingen die het zelfstandig neemt.

Member beslist of een naam en een leenlimiet aanvaardbaar zijn. Niet Library en ook niet de code die een lid aanmaakt. Als de validatie in Library.register_member zou zitten, zou elke andere route naar een Member die overslaan. Dan zou je nooit zomaar op de geldigheid van een lid kunnen vertrouwen.

Dat is de regel uit hoofdstuk 5 in een groter programma: een constructor moet weigeren iets ongeldigs aan te maken.

class Member:
    def __init__(self, member_id, name, loan_limit):
        if not name.strip():
            raise ValueError("a member needs a name")
        ...

Let ook op wat hier niet staat. Een lid weet niet welke items het heeft geleend. Dat is een gegeven over uitleningen. Door het op Member bij te houden, zouden twee plekken het over hetzelfde eens moeten blijven. Hoofdstuk 6 wees hierop: wanneer twee objecten allebei een relatie bijhouden, zullen ze uit elkaar gaan lopen.

Waarom wijst Member een lege naam af in zijn eigen constructor, in plaats van dat Library.register_member die afwijst?

Identiteit is niet hetzelfde als gelijkheid

Elk item heeft een item_id en elk lid een member_id, omdat de bewerkingen uit het contract identificatoren krijgen in plaats van objecten.

Dat is iets anders dan gelijkheid op basis van waarde uit hoofdstuk 7. Book is hier geen waardeobject: twee exemplaren van hetzelfde boek in de kast zijn twee verschillende dingen om uit te lenen, ook als titel en auteur overeenkomen. Hun item-id’s moeten verschillen als beide exemplaren in de catalogus staan. Laat __eq__ dus met rust. Identiteit is precies wat je wilt; met de id vindt de bibliotheek een exemplaar.

Wat Book en Game bevatten

Beide hebben een item_id en een title nodig, want dat hebben de bibliotheek en de rapporten van elk item nodig.

Verder verschillen ze, en daarom zijn er twee klassen. Een boek heeft een author. Een spel heeft een aantal players.

Beide beantwoorden ook twee vragen waarop de rest van het programma steunt:

class Book:
    def loan_period_days(self):
        return 21

    def describe(self):
        return f"{self.title} by {self.author}"

Dezelfde twee namen op Game, met andere antwoorden: één week in plaats van drie en een zin over spelers in plaats van een auteur.

Dat ze hier staan en niet in Library, volgt weer uit de regel over verantwoordelijkheid. Hoe lang een spel geleend mag worden, is een gegeven over spellen. Als een bibliotheek dat besliste, zou die elk mogelijk type item moeten kennen. In les 7 wordt het verschil tussen die twee ontwerpen concreet.

Opdracht

Schrijf de drie klassen voor de begrippen in models.py.

Book(item_id, title, author) en Game(item_id, title, players) slaan hun argumenten elk op als attributen met dezelfde namen. Geen van beide valideert al iets.

Beide beantwoorden ook de twee vragen die elk uitleenbaar item beantwoordt:

  • loan_period_days() geeft 21 terug voor een boek en 7 voor een spel.

  • describe() geeft respectievelijk "Solaris by Lem" en "Carcassonne (up to 5 players)" terug.

Member(member_id, name, loan_limit) slaat zijn drie argumenten op en weigert aangemaakt te worden als een van deze regels wordt overtreden:

  • name is leeg of bestaat alleen uit witruimte, of

  • loan_limit is kleiner dan 1.

Beide weigeringen werpen ValueError op met een melding die vertelt wat er mis was.

Houd de validatie binnen Member.__init__. Niets anders in het project mag een lid kunnen maken dat deze regels overtreedt.