0%

Dataclasses en waardeobjecten · oefening

Datagerichte objecten en objecten met veel gedrag

Dataclasses maken een bepaald soort klasse zo eenvoudig dat het de moeite waard is te vragen wanneer je er geen gebruikt. Het antwoord gaat niet over syntaxis. Het gaat over waar het object voor dient.

Twee soorten objecten

Try it

Points is datagericht. Het draagt een waarde en bijna alles wat je ermee doet wordt door iets anders gedaan. De volledige definitie is de lijst van wat het bevat.

QuizAttempt is rijk aan gedrag. Het draagt toestand, maar de reden dat het bestaat is wat het met die toestand doet. De attributen bepalen hoe het werkt, niet wat het is.

Beide zijn hier dataclasses en dat is prima. De poging registreert punten wanneer een antwoord wordt gegeven en behoudt daarmee de momentopnameregel uit hoofdstuk 6. In de oefening gebruikt de interne lijst field(default_factory=list, init=False, repr=False): init=False houdt die geschiedenis buiten de constructorargumenten en repr=False laat die weg uit de gegenereerde weergave.

Het onderscheid doet er om een andere reden toe.

Waar de methoden horen

Een datagerichte klasse trekt methoden aan die er niet bij horen. Kijk hoe dit misgaat:

@dataclass(frozen=True)
class Points:
    amount: int

    def format_for_report(self, width):        # display
        ...

    def convert_to_percentage(self, total):    # arithmetic about something else
        ...

    def save_to_database(self):                # storage
        ...

Elk werd toegevoegd omdat Points de nabijgelegen klasse was met het getal erin. Geen ervan gaat over punten. format_for_report hoort bij een rapport, convert_to_percentage heeft een totaal nodig dat Points helemaal niet hoeft te kennen, en save_to_database bindt een waardeobject aan een systeem waar het nooit van zou moeten horen.

De toets uit hoofdstuk 6 geldt nog steeds: welk object zou veranderen als de regel veranderde? Een waarde hoort methoden te behouden die over de waarde zelf gaan, ongeacht wat het omliggende programma doet.

Try it

Beide zijn gegevens over tijdsduren. total_seconds is dezelfde tijdsduur anders uitgedrukt en plus combineert twee tijdsduren tot een derde. Geen van beide noemt rapporten, opslag of quizzen, en beide zouden in een heel ander programma nog steeds logisch zijn.

Merk op dat plus een nieuwe Duration teruggeeft in plaats van een van de invoerwaarden te veranderen. Zo ziet een methode op een bevroren waarde eruit en het komt overeen met het gedrag van + op getallen.

Welke methode hoort bij een bevroren waarde Points?

Een model zonder eigen gedrag

Aan de andere kant ligt ook een fout, en die komt vaker voor.

Als elke klasse in een programma datagericht is en alle regels in functies staan die die klassen uit elkaar halen, zijn de klassen alleen dictionaries met betere reprs. Die opzet heet het anemische model, en hoofdstuk 1 van deze cursus begon ermee.

@dataclass
class Question:
    prompt: str
    answer: str
    points: int


def is_correct(question, response):        # belongs on Question
    return response == question.answer


def points_for(question, response):        # belongs on Question
    if is_correct(question, response):
        return question.points
    return 0

Beide functies lezen de attributen van één object en nemen een beslissing die dat object zelf zou kunnen nemen. Van Question een dataclass maken verwijderde de boilerplate; het gaf geen toestemming om het gedrag eruit te halen.

@dataclass genereert __init__, __repr__ en __eq__. Dat zijn de methoden waar niemand over nadenkt. Alles waarover je echt een beslissing hebt genomen hoort nog steeds in de klasse.

Een werkbare regel

Vraag waarvoor de klasse dient.

Een waarde. Een bevroren dataclass. Methoden die over de waarde gaan en nieuwe waarden teruggeven. Points, Duration, Question.

Een entiteit. Een dataclass met eq=False of een gewone klasse. Methoden genoemd naar gebeurtenissen, waarmee de toestand verandert. QuizAttempt, Learner.

Een service. Vaak helemaal geen gegevens, of alleen samenwerkingspartners. ScoreReport bewaart een poging en zet die om in regels.

De oefening bevat een programma waarin methoden in beide richtingen aan de verkeerde kant van deze grenzen zijn beland.

Opdracht

De methoden in dit programma zijn in beide richtingen verschoven. Zet ze waar ze horen.

Duration heeft format_for_report gekregen, wat weergavewerk is. Verplaats dat naar ScoreReport en geef Duration de ene methode die echt over tijdsduren gaat: plus(other), die een nieuwe Duration met de gecombineerde tijd teruggeeft.

Question is uitgehold. De functies is_correct en points_for op moduleniveau lezen elk één vraag en nemen een beslissing die de vraag zelf zou kunnen nemen. Verplaats beide als methoden naar Question en verwijder de functies.

QuizAttempt behoudt het gedrag en krijgt record(question, given), dat een momentopname (given, earned) in de interne lijst _results bewaart. De alleen-lezen property score berekent de score uit die ene geschiedenis en results() geeft een kopie terug. Vraag het aan de vraag in plaats van hier te vergelijken.

ScoreReport geeft regels terug en drukt niets af.

ScoreReport.lines() geeft twee strings terug: "<learner>: <score> points" en "took <minutes>m<seconds>s", met seconden aangevuld tot twee cijfers. Twee minuten en vijf seconden verschijnt bijvoorbeeld als "took 2m05s". Gebruik de gecombineerde tijdsduur van plus; tel elke volle 60 seconden bij de minuten op.