0%

Organizing a Program Across Files · practice

Separate Domain, Interface, and Entry Modules

Chapter 6 argued that a domain should compute and return, never print, and that presentation belongs at the edge of the program. That was a rule about classes. In a project it becomes a rule about files, and it gets easier to follow, because a file boundary is visible in a way a boundary is not.

The three layers

main.py       entry     what this program does when you run it
report.py     interface facts turned into text
models.py     domain    the rules, and the vocabulary they use
scoring.py    domain    rules with no state of their own

Dependencies point downward. main.py imports from the domain and report . report.py uses the scoring rules and may also import from models. Nothing in the domain imports anything above it, ever.

This one-way arrangement is the module dependency direction. It is the whole design: it lets the same models.py serve this program, a web page, and a test suite without knowing that any of them exist.

The test that keeps it honest

There is a question you can ask about any module, and it is much sharper than “does this feel like the right file?”:

If the program moved from a terminal to a web page, would this file change?

  • models.py: no. A question is a question.

  • scoring.py: no. Ninety percent is an A either way.

  • report.py: yes. Lines of text become HTML.

  • main.py: this wiring would be replaced by the web application’s entry code.

Files that answer “yes” are interface. Files that answer “no” are domain. When a file answers “partly”, it is doing two jobs and wants splitting.

Why must models.py never import from report.py?

Where the line falls in practice

The distinction is not always obvious, and two cases come up constantly.

Formatting a number. percentage(4, 10) returning 0.4 is domain: it is a fact. Rendering it as "40%" is interface, because the choice of a percent sign, decimal places, and whether to use a comma or a period is presentation.

Deciding a pass mark. Domain. A quiz’s pass mark is a rule about quizzes, not about how the result is shown, which is why it lived on Quiz back in Chapter 6.

The useful phrasing: the domain decides what is true, the interface decides how to say it.

A layer is not always a file

Four files for a sixty-line program is close to too many, and this project is right at the edge of where splitting starts to pay.

Do not read this chapter as an instruction to create four modules for every script. Read it as: when a program grows enough that you cannot hold it in your head, these are the seams to cut along, and they are almost always the same three.

Real projects grow more files inside each layer, not more layers. A larger version of this program would have models.py, attempts.py, and question_types.py in the domain, with an entry point for each way the application is started.

The exercise

report_lines moves out of main.py and into a report.py of its own. Lesson 7 then groups the modules in a package, and Lesson 8 wires the finished project together.

Task

Create report.py, the project’s interface layer, which this lesson’s editor owns.

Move report_lines(attempt) into it, unchanged, and add headline(quiz) returning "<title> (<total> points)".

report.py may import from scoring, because turning a score into a grade is a rule it needs. It must not import from main: nothing in the project may depend on the entry point.

It must also print nothing, either at import time or when its are called. A report returns text and lets the caller decide what to do with it.

Keep the current copy in main.py for now so Run still works. Submit checks report.py; Lesson 8 removes that copy when it imports the package report.