0%

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

Try it

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 Question entscheidet, ob eine eingegebene Antwort richtig ist.

  • Ein Response entscheidet, wie viele Punkte es verdient hat, und beschreibt sich mit describe() selbst.

  • Ein QuizAttempt meldet seinen eigenen score und correct_count.

  • ScoreReport ordnet 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.