0%

Objects Working Together · practice

Choose Who Owns a Responsibility

Every you write has to live in some class, and often two or three of them look like reasonable homes. Choosing badly does not break the program. It makes the next change harder, in a way that is invisible until you try to make it.

Here is a real decision. A quiz should be able to report whether a learner passed, using a rule like “at least 60 percent of the available points.”

Where does passed go?

Three plausible homes

On Quiz: quiz.passed(attempt). The quiz knows the total points available, which the rule needs.

On QuizAttempt: attempt.passed. The attempt knows the score, which the rule also needs.

On ScoreReport: report.passed(). Reporting is what a report does.

All three would work. Try each one against a question that has nothing to do with taste: which would have to change if the rule changed?

The pass mark is a property of the quiz. A practice quiz might pass at 50 percent and a final at 80. It is not a property of any one attempt, and it is certainly not a property of a report, which should be able to display a result without having any opinion about it.

So the threshold belongs to Quiz. But the comparison needs a score, which belongs to an attempt.

Splitting a rule across its owners

Try it

Two small methods instead of one. Quiz.is_passing(score) owns the threshold and the arithmetic. QuizAttempt.passed owns knowing which score to hand over.

Callers get the reading they wanted, attempt.passed, and the pass mark can move to 0.8 by touching one class.

Notice that is_passing takes a score rather than an attempt. A quiz does not need to know that attempts exist, and keeping it that way means the same quiz could grade a stored result, a projected score, or a practice run.

A new requirement says that a quiz marked "final" needs 80 percent to pass, while others need 60. Which class changes?

Questions that place a method

When you are unsure, these usually settle it:

  1. Which object holds the data the rule needs? If one object has all of it, start there.

  2. Which object would change if the rule changed? The rule belongs with the thing it is a fact about.

  3. What would the method be called? If a good name has to mention another class (calculate_attempt_score_for_quiz), it is probably in the wrong place.

  4. Does putting it here mean reaching through two objects? If so, the method wants to be one step closer.

The god object

The failure this chapter guards against has a name. A god object is a class that ends up owning everything, usually because it was the first class written and each new feature was easiest to add there.

It arrives one reasonable decision at a time. Quiz holds the questions, so scoring goes there. It has the scores, so reporting goes there. It has the report, so formatting goes there. Nothing is obviously wrong at any step, and eventually Quiz is nine hundred lines and every change touches it.

The signals are worth memorizing: a class much larger than its neighbors, a class that has to be imported everywhere, a class whose name has stopped describing what it does, and methods that need three unrelated pieces of state.

The exercise gives you exactly this situation, at a size you can fix in a few minutes.

Task

Quiz has become a god . It holds the questions, and it has also collected the scoring, the pass rule, and the report formatting.

Move each responsibility to the class that owns it:

  • Quiz keeps the questions, total_points, and the pass rule as is_passing(score). Its pass_mark stays a construction .

  • QuizAttempt gains answer(question, given), score, and passed, which asks its quiz.

  • ScoreReport gains summary(), returning "<learner>: <score> of <total>, <passed or failed>".

Delete each from Quiz once its work has moved. When you are finished, Quiz should contain nothing that mentions a learner or an attempt.

The starter’s driver already calls the new interface, so it will not run until you supply those methods. Once complete, it should print:

2 False
Mina: 2 of 5, failed