0%

Gültigen Objektzustand schützen · Übung

Zustand durch aussagekräftige Methoden ändern

Ein Question-Objekt lässt sich nun sicher erzeugen. Die Erzeugung ist allerdings nur die erste Grenze: Danach lassen sich seine Attribute weiterhin beschreiben.

Die meisten interessanten Objekte ändern sich. Ein Quizversuch erhält Antworten. Ein Punktestand steigt. In dieser Lektion geht es nicht darum, ob sich ein Objekt ändern darf, sondern darum, wie eine Änderung angefordert werden sollte.

Eine zweite Art von Wert

Hier ist ein Attempt: eine lernende Person, die ein Quiz bearbeitet, zunächst auf die naheliegende Weise umgesetzt.

Try it

Angenommen, eine Person beantwortet eine Frage für 2 Punkte richtig. Das Programm, das diese Klasse verwendet, muss die Aktualisierung selbst vornehmen:

Try it

Das funktioniert. Es bedeutet auch, dass jeder Programmteil, der eine Antwort erfasst, an beide Zeilen denken muss, in der richtigen Reihenfolge und mit der richtigen Punktzahl. Fehlt die zweite Zeile, passt der Punktestand unbemerkt nicht mehr zu den Antworten.

Schlimmer noch, nichts verhindert Folgendes:

Try it

Eine erfasste Antwort und ein Punktestand von 500. Das Objekt widerspricht sich selbst, und zwar durch eine gewöhnliche Zuweisung, die für sich genommen völlig vernünftig aussieht.

Gib der Änderung einen Namen

Die Lösung besteht nicht darin, Änderungen zu verbieten. Das Objekt soll die Änderung selbst durchführen, über eine Methode, deren Name beschreibt, was passiert ist:

Try it

Zwei Antworten, davon eine richtig, und ein Punktestand von 2. Der aufrufende Code muss nicht mehr wissen, wie der Punktestand berechnet wird. Er meldet ein Ereignis, und das Objekt ermittelt, was dieses Ereignis für seinen eigenen Zustand bedeutet.

Achte darauf, was gewandert ist. Die Regel „Eine richtige Antwort bringt die Punkte der Frage“ stand vorher im aufrufenden Code, wiederholt an jeder Stelle, die eine Antwort erfasste. Jetzt steht sie einmal im Objekt, das für den Punktestand verantwortlich ist.

Warum ist attempt.record(question, response) besser geeignet, Zustand zu ändern, als eine direkte Zuweisung an attempt.score?

Methoden nach Ereignissen benennen, nicht nach Zuweisungen

Ein nützlicher Test beim Benennen einer Methode: Beschreibt der Name etwas, das passiert ist, oder beschreibt er lediglich die Zuweisung?

SchwächerAussagekräftiger
set_score(value)record(question, response)
set_responses(items)record(question, response)
update_state(a, b)finish()

Eine Methode namens set_score ist eine verkleidete Zuweisung. Sie akzeptiert eine beliebige Zahl aus dem aufrufenden Code. Genau das war unsere Ausgangslage. record nimmt ein tatsächlich eingetretenes Ereignis entgegen und leitet die Folgen selbst daraus ab.

Ein Ereignis kann auch einen Lebenszyklus abschließen. finish() hält fest, dass ein Versuch keine Antworten mehr annimmt. Danach kehrt record() zurück, ohne Antworten oder Punktestand zu ändern. Die Schutzabfrage gehört an den Anfang von record(), denn die Bedeutung dieses Vorgangs hat sich geändert:

def record(self, question, response):
    if self.finished:
        return

    # Record the response and update the score together.

Es ist dieselbe Entwurfsentscheidung: Der aufrufende Code meldet ein Ereignis, und das Objekt entscheidet, welche Zustandsänderungen an diesem Punkt seines Lebens gültig sind.

Das bedeutet nicht, dass eine Klasse niemals einen einfachen Setter anbieten darf. Lektion 6 schreibt bewusst einen mit Prüfungen. Es erinnert daran, dass die erste Entwurfsfrage nach den Ereignissen im Leben eines Objekts fragt, nicht nach den Attributen, die du ändern möchtest.

In dieser Version lässt sich der Punktestand noch direkt zuweisen. Der Widerspruch von vorhin bleibt also möglich. Die nächste Lektion kennzeichnet diese Speicherung als intern; anschließend steuern Properties den Zugriff über die öffentliche Schnittstelle.

Aufgabe

Gib Attempt eine Methode record, damit das aufrufende Programm responses oder score nicht mehr selbst verändert.

record(question, response) soll die Antwort speichern und, wenn sie richtig ist, die Punkte dieser Frage zum Punktestand addieren. Auch eine falsche Antwort wird erfasst, bringt aber keine Punkte.

Vervollständige anschließend finish(), das den Versuch als abgeschlossen markiert. Ein abgeschlossener Versuch erfasst nichts mehr: Ein Aufruf von record nach finish() soll Antworten und Punktestand unverändert lassen.

Führe das Programm aus und prüfe, ob der ausgegebene Punktestand zu den Antworten darüber passt.