Objects Working Together · practice
Build Has-a Relationships
The Quiz you wrote holds Question Attempt from Chapter 5 holds results. Neither arrangement needed a name while there was only one of them, but a program with five classes needs a way to talk about how they fit together.
The relationship in both cases is the same one: has-a. A quiz has questions. An attempt has results.
Reading a design out loud
Try describing the pieces in plain sentences, using only “has a” and “is a”:
a Quiz has many Questions
a QuizAttempt has a Quiz
a QuizAttempt has many Responses
a Response has a Question
That is a design. It fits on four lines, it can be read by someone who does not know Python, and it commits you to something: each arrow says which object holds a reference to which, and therefore which one can ask the other for things.
Building objects out of other objects like this is called composition. It is the default way to structure a program, and it stays the default for the rest of this course. Chapter 9 introduces “is a” relationships, which are useful in a narrower set of situations than most introductions suggest.
Writing it down
Response asks the question when the answer arrives, then keeps that verdict and the points available at that moment. Chapter 5 used the same snapshot rule: changing a question later must not rewrite an answer that already happened.
Four classes, and the four lines of the design above map onto them one for one. QuizAttempt.__init__ takes a Quiz and keeps it. Response.__init__ takes a Question and keeps it. Each object holds the things it has.
A reference, not a copy
attempt.quiz and the quiz
Adding a question through quiz changed what attempt.quiz reports, because there is only one quiz. This is the two-names-for-one-object idea from Chapter 4, now spanning two classes.
This version deliberately follows the live quiz, so a newly added question also counts as remaining. The project will make a different choice for historical reports by capturing the starting quiz facts. It is also the reason the previous lesson was careful about handing out internal
A QuizAttempt holds a Quiz. What does that mean in memory?
What each class stopped needing to know
Look at what QuizAttempt.score does. It sums response.earned, and stops.
It does not know that a response earns points only when correct. It does not know that the points come from a question. It does not know how a question decides correctness. Each of those facts lives in exactly one class, and the class above it just asks.
Compare that with what one big class would look like: a Quiz holding parallel lists of prompts, answers, points, responses, and earned values, with every
Drawing the boundary
The useful question when you are unsure whether a class should hold another object or just its data: does the smaller thing have behavior of its own?
Response holds a Question rather than a prompt is_correct. If a response only ever needed to display the prompt, storing the prompt would be reasonable and simpler.
The exercise builds the Response and QuizAttempt classes above the Quiz you already have.
Task
Finish the class that sits above the supplied Response, following the design:
a Response has a Question
a QuizAttempt has a Quiz, and has many Responses
Response is supplied. It asks its question once when an answer arrives, then keeps snapshots of correctness and earned points so a later question edit does not rewrite history.
Build QuizAttempt: it holds its quiz and learner. answer(question, given) records one Response. score totals what the responses earned, correct_count counts the right ones, and remaining reports how many of the quiz’s questions have not been answered yet. For this small model, the caller answers each question in the quiz at most once. The exercise does not need to reject repeated or unrelated questions.
Keep each class ignorant of the others’ rules. QuizAttempt should never compare a response with an answer, and Response should never look inside the quiz.