0%

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 do change. A quiz attempt gains responses. A score goes up. The question this lesson answers is not whether an object may change, but how the change should be requested.

A second kind of value

Here is an Attempt: one learner working through a quiz, built the obvious way.

Try it

Now suppose a learner answers a question worth 2 points, correctly. The program using this class has to do the update itself:

Try it

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 . Miss the second line and the score silently stops matching the responses.

Worse, nothing stops this:

Try it

One response recorded and a score of 500. The object is internally contradictory, and it got that way through an ordinary that looks perfectly reasonable on the line where it was written.

Give the change a name

The fix is not to forbid change. It is to let the object perform the change itself, through a whose name says what happened:

Try it

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?

WeakerStronger
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 so the calling program never touches 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.