Zusammenarbeit zwischen Objekten · Übung
Arbeit delegieren
Response.__init__ bittet ein anderes Objekt um die Entscheidung, für die es zuständig ist:
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
Es beantwortet eine Frage, indem es jemand anderen fragt. Das verdient einen Namen: Delegation. Ein Objekt erhält eine Anfrage, erkennt, dass ein anderes Objekt die Antwort kennt, und gibt die Anfrage dorthin weiter.
Delegation macht aus einem Haufen Klassen einen Entwurf. Ohne sie übernimmt das oberste Objekt schließlich alles, weil nur es alles sehen kann.
Die Form einer delegierenden Methode
Response enthält keine Regel zur Richtigkeit. Es weiß, wen es fragen muss, und fragt bei Eingang der Antwort einmal. Wenn Fragen später ohne Beachtung der Groß- und Kleinschreibung oder mit numerischer Toleranz vergleichen, ist diese Klasse bereits fertig. Für Teilpunkte wäre eine umfassendere Frageoperation nötig, die verdiente Punkte statt eines booleschen Werts zurückgibt. Die festgehaltene Entscheidung verhindert außerdem, dass eine spätere Änderung der Frage die Vergangenheit umschreibt.
Das ist der Test für eine gute Delegation: Ändert sich dieser Code, wenn sich die Regel ändert? Falls nein, liegt die Verantwortlichkeit an der richtigen Stelle.
Delegieren ist mehr als Weiterleiten
Eine delegierende Methode trägt meist etwas Eigenes bei. Vergleiche diese beiden:
@property
def prompt(self):
return self.question.prompt # pure forwarding
@property
def earned(self):
return self._earned # the result captured when answered
Die zweite zeigt, weshalb Response seinen Platz im Entwurf verdient. Die Regel „Eine richtige Antwort bringt die Punkte der Frage“ ist weder eine Aussage über Fragen noch über Versuche. Genau dafür ist eine Antwort da.
Bei bloßem Weiterleiten lohnt sich dagegen ein kritischer Blick. Eine Klasse, deren Methoden alle wie die erste aussehen, fügt eine weitere Zugriffsstufe hinzu, aber keine Bedeutung. Manchmal ist das dennoch richtig, weil es aufrufenden Code von einer Struktur fernhält, die sich bald ändern wird. Oft bedeutet es, dass die Klasse überflüssig ist oder der aufrufende Code direkt das innere Objekt halten sollte.
QuizAttempt.score summiert response.earned über alle Antworten. Welche Änderung würde eine Überarbeitung von score erfordern?
Delegationsketten und eine, die du vermeiden solltest
Es gibt zwei kurze Wege. Die Entscheidung fällt einmal bei Eingang der Antwort:
Response.__init__
-> Question.is_correct
Später lesen Summen das gespeicherte Ergebnis, ohne die Frage erneut zu stellen:
QuizAttempt.score
-> Response.earned snapshot
Bei jedem Schritt fragt ein Objekt einen direkten Nachbarn. Diese Form solltest du anstreben.
Diese Form solltest du vermeiden:
total = 0
for response in attempt.responses():
if response.given == response.question.answer:
total = total + response.question.points
Hier greift aufrufender Code durch den Versuch und die Antwort bis zur Frage hindurch, um eine Regel nachzubauen, die diese drei Objekte selbst hätten übernehmen können. Eine solche Kette von Punktzugriffen bedeutet, dass der Code von der Struktur von Objekten abhängt, die ihm nie direkt übergeben wurden. Ändere Question, und diese Schleife scheitert, obwohl sie eine Frage nie ausdrücklich als eigenen Beteiligten genannt hat.
Das Anzeichen erkennst du leicht in deinem eigenen Code: a.b.c.d. Zwei Punkte auf fremden Objekten bedeuten meist, dass du das falsche Objekt gefragt hast.
Die Übung
Der Startcode enthält ein funktionierendes Programm mit einer Klasse ScoreReport, die für ihre Ausgabezeilen durch alle Objekte hindurchgreift. Verschiebe jede Entscheidung zu dem Objekt, das dafür verantwortlich ist, damit der Bericht am Ende fragt statt rechnet.
Aufgabe
ScoreReport greift derzeit durch zwei Objekte auf ein drittes zu und baut Regeln nach, die bereits anderswo existieren.
Verschiebe jede Entscheidung zum zuständigen Objekt:
Ein
Questionentscheidet, ob eine eingegebene Antwort richtig ist.Ein
Responseentscheidet, wie viele Punkte es verdient hat, und beschreibt sich mitdescribe()selbst.Ein
QuizAttemptmeldet seinen eigenenscoreundcorrect_count.ScoreReportordnet die erhaltenen Informationen lediglich zu Zeilen an.
Wenn du fertig bist, sollte ScoreReport die Gesamtwerte des Versuchs lesen und für jede Antwort describe() aufrufen. Es soll keine Antworten vergleichen, keine Punkte addieren und nicht wie in response.question.points durch eine Antwort auf deren Frage zugreifen. Einen direkten Partner zu lesen, etwa mit self.attempt.score, ist vorgesehen.
Der ausgegebene Bericht muss exakt so aussehen wie jetzt.