0%

Objects Working Together · practice

Keep Domain Logic Away from Input and Output

Every class you have written this chapter returns values. None of them print. That was deliberate, and it is worth saying why before the habit is broken by the first class that seems to need it.

Imagine QuizAttempt.answer had been written to be helpful:

Try it

It works, and for a small script it is genuinely convenient. The cost arrives later, all at once.

What the print costs

Ask what happens when the program grows:

  • Testing. The score can be checked directly, but checking the feedback requires capturing standard output.

  • A different interface. Putting this quiz on a web page means the message has to appear in HTML. The prints to a that no longer exists.

  • Language. Translating the feedback means editing scoring code.

  • Reuse. Scoring stored attempts to produce a summary now also prints feedback nobody asked for.

  • Silence. There is no way to score without talking.

Each of these is the same problem: a decision about presentation was written into an whose job is rules.

Separating the two

The word for the rules part is the domain: questions, responses, scores, passing. Those exist whether or not anyone is looking. Around it sits the interface: printing, reading input, formatting, colors, HTML.

Keep domain objects silent. Let them compute, decide, and return. Do the talking at the edge of the program:

Try it

The same output. Now answer returns a fact, feedback turns a fact into a sentence, and print puts a sentence on a screen. Three jobs, three places, and only the last one cares that a terminal exists.

Which of these belongs inside a domain class such as QuizAttempt?

The shape this gives a program

Sorted by how often it changes, a program made this way has layers:

entry point     input(), print(), the loop that drives everything
formatting      ScoreReport, feedback(): facts into text
domain          Quiz, Question, QuizAttempt, Response: the rules

Dependencies point downward. Formatting knows about the domain, and the domain knows nothing about formatting. That is why the same Quiz can serve a terminal program, a web page, and a test suite without noticing the difference.

A quick way to check your own code: if you deleted every print and input from the program, would the domain classes still make sense and still be testable? If yes, the line is in the right place.

Being reasonable about it

This is not a rule against ever printing. A ten-line script that prints as it goes is fine, and adding three layers to it would be worse.

It is also not a rule that domain code may never report anything. Raising , as Chapter 5 did, is a domain object reporting a problem without deciding how it is shown. The caller catches it and chooses between a red banner, a log line, or a retry. Chapter 12 builds on exactly that separation.

The exercise takes a working quiz program with printing scattered through its classes and pulls the presentation out to the edge.

Task

The starter mixes answer feedback with scoring. Move the printing out of QuizAttempt so the domain classes return facts instead:

  • QuizAttempt.answer(question, given) returns whether the response was correct.

  • QuizAttempt.score and correct_count stay as properties, and responses() returns a copy of its response snapshots.

  • ScoreReport.review_lines() returns a of : a heading Review of <quiz title>, then one line per response in recorded order. Each response line begins with two spaces and reads <prompt> -> <given> (right) or <prompt> -> <given> (wrong).

In run_quiz, print the supplied feedback(question, correct) result after each answer, then the review lines, then the existing score summary. Only run_quiz prints; constructing questions, scoring, and reading domain facts must produce no output.

The supplied program should produce:

Correct!
Not quite. The answer was def.
Review of Chapter 5 Review
  What does len('cat') return? -> 3 (right)
  Which keyword starts a function? -> class (wrong)
Mina: 2 of 5