0%

Objects Working Together · practice

Delegate Work

Response.__init__ asks another to make the decision it owns:

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

It answers a question by asking someone else. That is worth a name: delegation. An object receives a request, decides it is not the one who knows, and passes it to the object that does.

Delegation is what makes a design out of a pile of classes. Without it, the object at the top ends up doing everything, because it is the only one that can see everything.

The shape of a delegating method

Try it

Response contains no rule about correctness. It knows who to ask, and it asks once when the answer arrives. If questions gain case-insensitive matching or numeric tolerance, this class is already finished. A partial-credit design would need a richer question operation that returns earned points rather than a . Keeping the verdict also prevents a later question edit from rewriting history.

That is the test for a good delegation: when the rule changes, does this code change? If the answer is no, the responsibility is in the right place.

Delegation is not just forwarding

A delegating usually adds something of its own. Compare these two:

@property
def prompt(self):
    return self.question.prompt          # pure forwarding

@property
def earned(self):
    return self._earned                  # the result captured when answered

The second is where the Response earns its place in the design. The rule “a correct response is worth the question’s points” is not a fact about questions, and it is not a fact about attempts. It is exactly what a response is for.

Pure forwarding, on the other hand, is worth being suspicious of. A class whose methods all look like the first one is a layer that costs a lookup and adds no meaning. Sometimes that is still right, because it keeps callers away from a structure that is about to change. Often it means the class should not exist, or the caller should be holding the inner object directly.

QuizAttempt.score sums response.earned across its responses. Which change would force score to be rewritten?

Chains of delegation, and one to avoid

There are two short paths. The decision happens once when the answer arrives:

Response.__init__
  -> Question.is_correct

Later, totals read the stored result without asking the question again:

QuizAttempt.score
  -> Response.earned snapshot

Every step is one object asking a direct neighbor. That is the shape to aim for.

Here is the shape to avoid:

total = 0
for response in attempt.responses():
    if response.given == response.question.answer:
        total = total + response.question.points

This is a caller reaching through the attempt, through the response, and into the question, to reimplement a rule that all three of them could have handled. There is a name for reaching across a chain of dots like this: it means the code depends on the shape of objects it was never introduced to. Change Question and this breaks, even though it never mentioned a question by name.

The signal is easy to spot in your own code: a.b.c.d. Two dots on other people’s objects usually means you asked the wrong object a question.

The exercise

The starter has a working program with a ScoreReport class that reaches through everything to build its lines. Move each decision to the object that owns it, so the report ends up asking rather than calculating.

Task

ScoreReport currently reaches through two to reach a third, and rebuilds rules that already exist elsewhere.

Move each decision to the object that owns it:

  • a Question decides whether a response is correct;

  • a Response decides what it earned, and describes itself with describe();

  • a QuizAttempt reports its own score and correct_count;

  • ScoreReport only arranges what it is told into lines.

When you are done, ScoreReport should read the attempt’s totals and call each response’s describe(). It should not compare answers, add points, or reach through a response to its question as in response.question.points. Reading a direct collaborator, such as self.attempt.score, is expected.

The printed report must come out exactly as it does now.