Objects Working Together · practice
Delegate Work
Response.__init__ asks another
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
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
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
@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
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
Move each decision to the object that owns it:
a
Questiondecides whether a response is correct;a
Responsedecides what it earned, and describes itself withdescribe();a
QuizAttemptreports its ownscoreandcorrect_count;ScoreReportonly 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.