Protecting Valid Object State · practice
Change State Through Meaningful Methods
A Question is now safe to create. Construction is only the first boundary, though: its attributes are still writable afterward.
Most interesting
A second kind of value
Here is an Attempt: one learner working through a quiz, built the obvious way.
Now suppose a learner answers a question worth 2 points, correctly. The program using this class has to do the update itself:
That works. It also means every part of the program that records a response has to remember both lines, in the right order, with the right point
Worse, nothing stops this:
One response recorded and a score of 500. The object is internally contradictory, and it got that way through an ordinary
Give the change a name
The fix is not to forbid change. It is to let the object perform the change itself, through a
Two responses, one of them correct, and a score of 2. The caller no longer knows or cares how a score is calculated. It says what happened in the world, and the object works out what that means for its own state.
Notice what moved. The rule “a correct response is worth the question’s points” used to live in the calling code, repeated wherever a response was recorded. Now it exists once, inside the object that owns the score.
Why is attempt.record(question, response) a better way to change state than assigning to attempt.score directly?
Name methods after events, not assignments
A useful test when naming a method: does the name describe something that happened, or does it just describe the assignment?
| Weaker | Stronger |
|---|---|
set_score(value) | record(question, response) |
set_responses(items) | record(question, response) |
update_state(a, b) | finish() |
A method called set_score is an assignment wearing a costume. It accepts any number the caller invents, which is the situation we started in. record accepts a thing that actually occurred, and derives the consequences itself.
An event can also close a lifecycle. finish() records that an attempt is no longer accepting answers. Once that event has happened, record() returns without changing either responses or score. The guard belongs at the start of record() because that is the operation whose meaning has changed:
def record(self, question, response):
if self.finished:
return
# Record the response and update the score together.
This is still the same design move: callers report an event, and the object decides which state changes are valid at that point in its life.
This is not a rule that a class may never expose a simple setter. Lesson 6 writes one deliberately, with checks. It is a reminder that the first design question is what events an object experiences, not which attributes you want to poke.
The score is still directly assignable in this version, so the contradiction from earlier is still possible. The next lesson marks that storage as internal; properties will then control access through the public interface.
Task
Give Attempt a record responses or score itself.
record(question, response) should store the response and, when the response is correct, add that question’s points to the score. An incorrect response is still recorded, but adds nothing.
Then finish finish(), which marks the attempt as complete. A finished attempt records nothing further: calling record after finish() should leave the responses and the score unchanged.
Run the program and check that the printed score matches the responses above it.