Samenwerkende objecten · oefening
Delegeer werk
Response.__init__ vraagt een ander object de beslissing te nemen die bij dat object hoort:
def __init__(self, question, given):
self.question = question
self.given = given
self._correct = question.is_correct(given)
self._available = question.points
self._earned = self._available if self._correct else 0
Het beantwoordt een vraag door iemand anders iets te vragen. Daar hoort een naam bij: delegatie. Een object ontvangt een verzoek, bepaalt dat het zelf niet degene is die het antwoord weet en geeft het door aan het object dat dat wel weet.
Delegatie maakt van een stapel klassen een ontwerp. Zonder delegatie doet het object bovenaan uiteindelijk alles, omdat het als enige alles kan zien.
De vorm van een delegerende methode
Response bevat geen regel over juistheid. Het weet wie het moet vragen en doet dat één keer wanneer het antwoord binnenkomt. Als vragen hoofdletterongevoelige vergelijking of numerieke tolerantie krijgen, is deze klasse al klaar. Een ontwerp met gedeeltelijke punten zou een uitgebreidere vraagbewerking nodig hebben die verdiende punten teruggeeft in plaats van een booleaanse waarde. Door het oordeel te bewaren, voorkomt het ook dat een latere wijziging aan de vraag de geschiedenis herschrijft.
Dat is de toets voor goede delegatie: verandert deze code als de regel verandert? Als het antwoord nee is, zit de verantwoordelijkheid op de juiste plek.
Delegatie is meer dan doorsturen
Een delegerende methode voegt meestal zelf iets toe. Vergelijk deze twee:
@property
def prompt(self):
return self.question.prompt # pure forwarding
@property
def earned(self):
return self._earned # the result captured when answered
In de tweede verdient Response zijn plek in het ontwerp. De regel ‘een juist antwoord levert de punten van de vraag op’ is geen gegeven over vragen en ook geen gegeven over pogingen. Het is precies waar een antwoord voor dient.
Puur doorsturen mag je daarentegen best kritisch bekijken. Een klasse waarvan alle methoden op de eerste lijken, vormt een laag die een extra opzoekstap kost en geen betekenis toevoegt. Soms is dat toch juist, omdat het aanroepers weghoudt van een structuur die binnenkort verandert. Vaak betekent het dat de klasse niet zou moeten bestaan, of dat de aanroeper het binnenste object rechtstreeks zou moeten bewaren.
QuizAttempt.score telt response.earned op voor alle antwoorden. Welke wijziging zou vereisen dat score wordt herschreven?
Delegatieketens, en één om te vermijden
Er zijn twee korte routes. De beslissing wordt één keer genomen wanneer het antwoord binnenkomt:
Response.__init__
-> Question.is_correct
Later lezen de totalen het opgeslagen resultaat zonder het opnieuw aan de vraag te vragen:
QuizAttempt.score
-> Response.earned snapshot
Bij elke stap vraagt één object iets aan een directe buur. Dat is de vorm om naar te streven.
Dit is de vorm om te vermijden:
total = 0
for response in attempt.responses():
if response.given == response.question.answer:
total = total + response.question.points
Hier reikt een aanroeper via de poging en het antwoord tot in de vraag, om een regel opnieuw te implementeren die alle drie hadden kunnen afhandelen. Zo’n keten van punten doorlopen betekent dat de code afhangt van de vorm van objecten waarmee die nooit rechtstreeks kennismaakte. Verander Question en deze lus gaat stuk, hoewel die nergens een vraag bij naam noemde.
Het signaal is makkelijk te herkennen in je eigen code: a.b.c.d. Twee punten op objecten van anderen betekenen meestal dat je het verkeerde object iets hebt gevraagd.
De oefening
De startcode bevat een werkend programma met een klasse ScoreReport die overal doorheen reikt om de regels op te bouwen. Verplaats elke beslissing naar het object waar die hoort, zodat het rapport uiteindelijk vraagt in plaats van berekent.
Opdracht
ScoreReport reikt nu door twee objecten heen om bij een derde te komen en bouwt regels opnieuw op die elders al bestaan.
Verplaats elke beslissing naar het object waar die hoort:
een
Questionbepaalt of een antwoord juist is;een
Responsebepaalt wat die verdiende en beschrijft zichzelf metdescribe();een
QuizAttemptmeldt de eigenscoreencorrect_count;ScoreReportordent alleen wat het krijgt doorgegeven in regels.
Als je klaar bent, hoort ScoreReport de totalen van de poging te lezen en voor elk antwoord describe() aan te roepen. Het mag geen antwoorden vergelijken, punten optellen of via een antwoord naar de vraag reiken zoals in response.question.points. Een directe samenwerkingspartner lezen, zoals self.attempt.score, is wel de bedoeling.
Het afgedrukte rapport moet er precies zo uitkomen als nu.