0%

Objects Working Together · practice

Read and Draw Object Collaborations

You have now written five classes that work together. Someone else joining this program would have to read all five to find out how. There is a faster way, and it is the last skill this chapter teaches: describing a design without writing it.

A design on five lines

The notation from Lesson 3 is enough for most conversations:

a Quiz         has many  Questions
a QuizAttempt  has a     Quiz
a QuizAttempt  has many  Responses
a Response     has a     Question
a ScoreReport  has a     QuizAttempt

Read it as a map of who can ask whom. A ScoreReport can reach a QuizAttempt, and through it a Quiz, but a Question can reach nothing at all. Questions sit at the bottom: they answer, and never ask.

That last observation is worth more than it looks. The with no arrows leaving them are the ones you can test on their own, reuse anywhere, and change with confidence. When a design has none, everything depends on everything.

The same design as a picture

For anything with more than a handful of classes, a sketch reads faster:

A one-way ownership graph: ScoreReport holds QuizAttempt; QuizAttempt holds Quiz and Responses; Quiz holds Questions; and each Response holds one Question.

Arrows point from the holder to the thing it holds. This is not a formal notation, and it does not need to be. Its whole job is to fit on one screen and let you notice things, such as: Question has two arrows pointing at it and none pointing away, and there is no path back up from a Question to the Quiz that holds it.

What to look for in a drawing

Once a design is drawn, some problems become visible without reading any code.

Arrows both ways. If Quiz points to QuizAttempt and QuizAttempt points back to Quiz, ask whether both directions are needed. The relationship can couple changes to the two classes. Object references do not require both classes to import each other, but unnecessary dependencies make later organization harder. Chapter 10 examines imports.

One object with arrows to everything. That is the god object from Lesson 5, visible as a shape.

A long chain. ScoreReport -> QuizAttempt -> Quiz -> Question is fine while each step asks only its neighbor. It becomes a problem when the report starts reaching all the way down, which shows up in code as report.attempt.quiz.questions()[0].points.

An object nothing points to. Check who creates and uses it. It may be an entry point, an omitted caller may use it, or it may be unnecessary.

A design shows Quiz with an arrow to QuizAttempt, and QuizAttempt with an arrow back to Quiz. What does that suggest?

Drawing before writing

The most useful moment for this is before any class exists.

Take the requirement “a learner can retake a quiz, and we keep every attempt.” Write the nouns down, then the relationships:

a Learner      has many  QuizAttempts
a QuizAttempt  has a     Quiz
a QuizAttempt  has a     started_on day

Three lines, and a decision has already been made: attempts belong to a learner rather than to a quiz. That has consequences, because “show me everything Mina did” is now easy and “show me everyone who took the final” is now a search. Noticing this on paper costs a minute. Noticing it after writing four classes costs an afternoon.

The rule of thumb: sketch when you have more than about three classes, or whenever you catch yourself unsure which object should hold something.

The exercise

You are given a written design and a set of half-built classes. Make the code match the design, then use the design to answer a question about a change nobody has made yet.

Task

Finish Learner so the code matches this design:

a Learner      has many  QuizAttempts
a QuizAttempt  has a     Quiz
a QuizAttempt  has many  Responses
a Response     has a     Question
a Quiz         has many  Questions

Question, Quiz, Response, and QuizAttempt are supplied collaborators. Learner(name) keeps its public name. start(quiz) creates, remembers, and returns a new QuizAttempt for that quiz. attempt_count reports how many attempts were started. best_score reports the highest current attempt score, or 0 when there are no attempts. history() returns a new of the attempts in the order they were started; changing that list must not change the learner’s history.

Read the supplied classes against the arrows before you implement Learner. That keeps the exercise about interpreting a collaboration drawing rather than rebuilding four earlier lessons at once.

Follow the arrows. Nothing points from a Quiz back to a QuizAttempt, so a quiz must never mention attempts or learners. Nothing points from a Question anywhere, so a question keeps answering and never asks.

The program prints a short history for one learner across two attempts at the same quiz.