runtime
196 / 250
lesson 196 Software Architecture · advanced

Separation of concerns: parse, compute, present

A script that reads, calculates and prints in one loop works until someone needs the numbers somewhere else. Splitting it into parse, compute and present functions makes each part reusable and testable on its own.

You'll be able to
  • Separate parsing, computing and presenting into functions with one job each
  • Keep the core logic pure: data in, data out, no printing or file access
  • Reuse the same core with a different input source and a different output
The idea

Most programs do three different kinds of work: get data in (read a file, call an API, parse text), decide something about it (totals, rules, filters), and show the result (print, write a file, return JSON). Mixing them in one loop makes every part hard to change: the totals can't be reused for a dashboard, and testing the arithmetic requires a file on disk.

The usual structure is a functional core with an imperative shell: pure functions in the middle that take data and return data, and a thin layer at the edges that does the input and output and connects the pieces with return values (the Fundamentals Integration module started this; here it becomes the design).

Run each example to see what it prints. Running examples doesn't count toward progress.

example-1.py · An order report in three parts
orders_csv = """order_id,status,total
O100282,delivered,35.84
O100376,returned,91.47
O100918,delivered,55.72
"""

def parse_orders(text):
    orders = []
    for line in text.strip().splitlines()[1:]:
        order_id, status, total = line.split(",")
        orders.append({"order_id": order_id, "status": status, "total": float(total)})
    return orders

def summarise(orders):
    summary = {}
    for order in orders:
        entry = summary.setdefault(order["status"], {"count": 0, "revenue": 0.0})
        entry["count"] += 1
        entry["revenue"] += order["total"]
    return summary

def render(summary):
    lines = []
    for status in sorted(summary):
        entry = summary[status]
        lines.append(f"{status:<10}{entry['count']:>3}{entry['revenue']:>10.2f}")
    return "\n".join(lines)

print(render(summarise(parse_orders(orders_csv))))

The payoff comes when a requirement changes. Orders now also arrive as JSON from the web shop's API, and a dashboard wants the numbers as JSON. Only the edges change: the core is reused untouched.

example-2.py · Same core, new edges
import json

def summarise(orders):
    summary = {}
    for order in orders:
        entry = summary.setdefault(order["status"], {"count": 0, "revenue": 0.0})
        entry["count"] += 1
        entry["revenue"] += order["total"]
    return summary

api_body = '[{"order_id": "O101219", "status": "delivered", "total": 246.6}, {"order_id": "O103722", "status": "cancelled", "total": 403.26}]'
summary = summarise(json.loads(api_body))
print(json.dumps(summary, sort_keys=True))
assert summarise([]) == {}
assert summarise([{"status": "returned", "total": 5.0}]) == {"returned": {"count": 1, "revenue": 5.0}}
print("core checks pass")

When to split: as soon as a function both does I/O and makes decisions, or when you can't test a rule without creating files. Name functions after their one job. Mistakes to avoid: printing inside the core (callers can't reuse the result); passing file paths deep into the logic; and the opposite extreme, splitting a five-line script into ten files. Split along the three concerns first; further splits come when a part grows.

Review
  • Three concerns: get the data, decide, show the result.
  • Pure core (data in, data out) with input and output at the edges, connected by return values.
  • Separated layers can be reused with new inputs and outputs, and tested without files.
  • Next: settings that change between environments, kept out of the code.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

Separation of concerns: parse, compute, present — Software Architecture · Runtime