0%

Slotproject: een uitleenbibliotheek voor de buurt · oefening

Implementeer het waardeobject Loan

Book, Game en Member zijn allemaal dingen met een identiteit. Twee leden die Mina heten zijn twee mensen. De bibliotheek houdt ze uit elkaar met hun id.

Bij een uitlening werkt dat anders.

Wat een uitlening tot een waarde maakt

Een uitlening is een record: dit item, aan dit lid, vanaf deze dag, uiterlijk op die dag terug. Twee uitleningen waarbij alle vier overeenkomen, zijn geen twee uitleningen. Het is hetzelfde feit dat twee keer is vastgelegd.

Hoofdstuk 8 had een naam voor zulke objecten: waardeobjecten. Wat ze bevatten bepaalt ze volledig, ze vergelijken op inhoud en ze veranderen niet na het aanmaken. Dat past precies bij een uitlening. Daarom vraagt het contract hier om een dataclass en bij Book niet.

from dataclasses import dataclass


@dataclass(frozen=True)
class Loan:
    item_id: str
    member_id: str
    checkout_day: int
    due_day: int

frozen=True doet wezenlijk werk. Als een uitlening ter plekke bewerkt kon worden, zou elke code die er een heeft ongemerkt de uiterste inleverdag kunnen veranderen, zonder dat de bibliotheek dat wist. Bevroren betekent dat je een uitlening alleen kunt veranderen door haar te vervangen. Dat doet de bibliotheek bewust.

Je krijgt __init__, een leesbare __repr__ en gelijkheid op basis van waarde cadeau. Alle drie zijn later belangrijk: de repr verschijnt wanneer een test faalt en dankzij de gelijkheid kan een test assert loan in library.active_loans() schrijven.

Id’s opslaan, geen objecten

Kijk wat de uitlening bevat: item_id en member_id, geen Book en Member.

Dat is een bewuste keuze die uitleg verdient, want het alternatief is verleidelijk. Als je de objecten zou opslaan, kon je loan.item.title schrijven in plaats van het item op te zoeken.

Er zijn twee redenen om dat niet te doen. De bibliotheek beheert de catalogus al. Een uitlening die het item bevat, zou dus een tweede plek zijn waar dezelfde relatie bestaat. Hoofdstuk 6 waarschuwde daar precies voor. Bovendien zou een bevroren uitlening met een veranderbare Member slechts oppervlakkig bevroren zijn: de verwijzing op de uitlening zou je niet opnieuw kunnen toewijzen, maar het lid erin zou wel kunnen veranderen. Een uitlening met id’s is tot in alle onderdelen onveranderbaar.

Dat heeft wel een prijs: om de titel bij een uitlening te rapporteren, moet je die aan de bibliotheek vragen. Dat is de juiste richting voor de afhankelijkheid.

Loan gebruikt frozen=True. Wat zou nog veranderbaar zijn als het een Member-object bevatte in plaats van een member_id?

Een klein stukje gedrag

Een waardeobject mag vragen over zichzelf beantwoorden. Hoofdstuk 8 legde uit dat een waardeobject dat alleen gegevens bevat vaak een gemiste kans is.

is_overdue(on_day) ligt hier voor de hand:

    def is_overdue(self, on_day):
        return on_day > self.due_day

Merk op dat de methode de dag meekrijgt in plaats van een klok uit te lezen, net als overdue_loans(on_day). Let ook op waar de regel staat: of een uitlening te laat is, is een gegeven over die uitlening. Daarom hoort de vergelijking op Loan en schrijf je die niet opnieuw op elke plek die haar nodig heeft.

Te laat betekent >, niet >=. Een item dat uiterlijk op dag 20 terug moet zijn, is op dag 20 nog niet te laat; dat is het op dag 21. Het verschil is één toetsaanslag. Zo’n keuze verdient een test op de grens, precies zoals les 5 van hoofdstuk 13 betoogde.

Opdracht

Voeg Loan toe aan models.py en laat Book, Game en Member zoals ze zijn.

Maak er een bevroren dataclass van met vier velden, in deze volgorde: item_id, member_id, checkout_day, due_day. De eerste twee zijn strings, de laatste twee zijn gewone gehele dagnummers.

Geef de klasse één methode, is_overdue(on_day), die teruggeeft of de uitlening op die dag te laat is. Een uitlening die uiterlijk op dag 20 terug moet zijn, is op dag 20 nog niet te laat.

Sla id’s op, geen Book- of Member-objecten. De bibliotheek beheert die objecten en een bevroren uitlening met een veranderbaar object zou niet echt bevroren zijn.