Grade by Grade
C. Designing Modular Programs · Modular and Object-Oriented Design

Functional Decomposition: Designing Subprograms Top-Down

C1.3 C1.4About 70 minutes
Learning goals
  • I can break a problem into a hierarchy of subprograms, from a general main() down to small helpers.
  • I can draw a structure chart for a decomposition and turn it into Python functions.
  • I can judge a decomposition by cohesion, coupling and the single-responsibility principle.
  • I can separate pure computation from input and output so that functions are easy to test and reuse.
  • I can refactor a long function by extracting smaller functions without changing what the program does.

Words to know

functional decomposition
Designing a program by splitting its main task into subtasks, each done by a subprogram, and splitting those again until each piece is simple.
stepwise refinement
Writing a solution first in broad steps, then refining each step into more detailed steps or calls.
single-responsibility principle
A design rule: each function (or class) should have one job and one reason to change.
cohesion
How closely the statements inside one module belong together. High cohesion is good.
coupling
How much one module depends on the inside details of another. Low coupling is good.
pure function
A function whose result depends only on its arguments and that changes nothing outside itself.
side effect
Anything a function does besides returning a value: printing, reading input, writing a file, or changing a global or a passed-in list.
refactoring
Changing the structure of code to make it clearer without changing what it does.
stub
A placeholder function that has the right header but a simple temporary body, so the rest of the program can run.

One big job, many small jobs

Your school's winter carnival has a snowshoe race. The organizer hands you a list of names and times and asks for a ranked report with an average. You could write it as one long block of code. It would work, but it would be hard to read, hard to test and impossible to reuse for the cross-country ski race next week.

Professional programmers almost never write a program as one block. They decompose it: split the big job into smaller jobs, give each job to its own subprogram, and connect the pieces through parameters and return values. The Ontario curriculum calls this functional decomposition, and it is the first design skill of this unit.

Remember from Grade 11

In ICS3U you wrote functions with parameters and return values, kept variables local, used stepwise refinement and drew structure charts, and met the ideas of high cohesion and low coupling. This lesson takes those ideas further: how to decide where one function should end and the next begin.

A note about code on this site: programs in grey code boxes can be copied and run. Code shown inside practice questions and answers uses special spaces for indentation, so type it yourself instead of copying it.

Top-down design in three moves

  1. State the whole task in one sentence. "Read the race results, rank them, and print a report with the average." That sentence becomes main().
  2. List the major steps. Read and parse the data; rank it; compute the average; print the report. Each step becomes a function that main() calls.
  3. Refine each step until it is simple. Parsing all lines needs a way to parse one line, so parse_all() calls parse_line(). Printing needs times written as minutes and seconds, so print_report() calls format_time().

Stop refining when a function is short (often 3 to 15 lines), has a name that says exactly what it does, and could be tested by itself. The result is a hierarchy: general functions at the top, specific helpers at the bottom.

main()parse_all()rank()average_seconds()print_report()parse_line()by_time()format_time()Each line means “calls”. Higher boxes are more general; lower boxes do one small job.
The structure chart for the race report. main() sits at the top and only coordinates; each box below it does one job, and the bottom row are small helpers that can be tested on their own. (Strictly, rank() hands by_time to sorted(), which calls it.)

What makes a good subprogram?

  • Single responsibility. It does one job. A good test: you can describe it in one sentence without the word "and". average_seconds computes an average. It does not also print it.
  • High cohesion. Every line inside it serves that one job.
  • Low coupling. It gets everything it needs through parameters and gives its answer back with return. It does not read or change global variables, and it does not depend on how another function works inside.
  • A clear interface. The signature (name, parameters, return value) plus a docstring tells a caller everything. The caller should never need to read the body.
  • The right level of abstraction. main() speaks in big steps (rank, print_report); it should not also contain low-level details such as seconds % 60.

Separate computing from input and output

A strong habit is to keep computation (parsing, ranking, averaging) in pure functions and to keep input and output (input(), print(), files) in a few separate functions near the top of the chart. Pure functions are easy to test: call them with known arguments and check the answer. They are also easy to reuse, because a function that returns a value can be used by a text program, a web page or a game, while a function that prints can only print.

Pure functionargsaverage()valuesame input → same output, nothing elsechanges: easy to test and reuseFunction with side effectsreport()printglobalfile / screen
A pure function only takes arguments and returns a value. A function with side effects also prints, writes files or changes global data. Keep most functions pure and push side effects to a few clearly named output functions.
Worked example

Example 1: Decomposing the race report

Write the snowshoe race report so that it follows the structure chart. Each line of data looks like "Ben,298" (a name and a time in whole seconds).

  1. main() states the plan in three calls: parse the data, rank it, print the report with the average.
  2. parse_line(line) splits one line at the comma and returns a tuple (name, seconds), converting the seconds with int().
  3. parse_all(lines) calls parse_line once per line and returns the list of tuples. It does not need to know how a line is split.
  4. rank(results) returns a new list sorted by time. sorted() accepts a key function; we pass by_time, a one-line helper that returns the seconds part of a tuple. Passing a function as an argument is itself a powerful reuse tool.
  5. average_seconds and format_time are pure helpers. seconds // 60 gives whole minutes and seconds % 60 the leftover seconds; :02d pads the seconds to two digits so 5 seconds shows as 05.
  6. print_report is the only function that prints. enumerate(ranked, start=1) numbers the places from 1.

The full program and its output are below. The average is (312 + 298 + 305 + 330) ÷ 4 = 311.25 s, which rounds to 311 s = 5:11.

"""Snowshoe race report: a functional-decomposition example."""
RESULTS = ["Amara,312", "Ben,298", "Chloé,305", "Dev,330"]
def parse_line(line):
"""Split "name,seconds" into a (name, seconds) tuple."""
name, secs = line.split(",")
return name, int(secs)
def parse_all(lines):
"""Return a list of (name, seconds) tuples, one per line."""
results = []
for line in lines:
results.append(parse_line(line))
return results
def by_time(result):
"""Sort key: the seconds part of a (name, seconds) tuple."""
return result[1]
def rank(results):
"""Return a new list sorted from fastest to slowest."""
return sorted(results, key=by_time)
def average_seconds(results):
"""Return the mean time in seconds (results must not be empty)."""
total = 0
for name, secs in results:
total += secs
return total / len(results)
def format_time(seconds):
"""Turn a whole number of seconds into "m:ss" text."""
return f"{seconds // 60}:{seconds % 60:02d}"
def print_report(ranked, avg):
"""Display the ranked results and the average time."""
print("Snowshoe race results")
for place, (name, secs) in enumerate(ranked, start=1):
print(f"{place}. {name:<8} {format_time(secs)}")
print("Average time:", format_time(round(avg)))
def main():
results = parse_all(RESULTS)
ranked = rank(results)
print_report(ranked, average_seconds(results))
main()

race_report.py: every function has one job, and only print_report() prints.

Snowshoe race results
1. Ben 4:58
2. Chloé 5:05
3. Amara 5:12
4. Dev 5:30
Average time: 5:11

Output of the program above (what Python prints).

Develop top-down with stubs

You do not have to write the functions in order. Write main() first, then give every other function a stub body such as return [] or return 0. The program runs from the start, and you replace one stub at a time, testing each before you move on. This is how a team can split the work: once the signatures are agreed, each person fills in different functions.

Worked example

Example 2: Refactoring a long function

This function works, but it computes, decides and prints all in one place. Refactor it into single-responsibility functions without changing its output.

def report(marks):

    total = 0

    for m in marks:

        total += m

    avg = total / len(marks)

    print("Average:", round(avg, 1))

    high = marks[0]

    for m in marks:

        if m > high:

            high = m

    print("Highest:", high)

    if avg >= 80:

        print("Level 4")

    elif avg >= 70:

        print("Level 3")

    else:

        print("Below Level 3")

With report([84, 71, 90, 77]) it prints Average: 80.5, Highest: 90 and Level 4.

  1. Find the separate jobs: computing the average, finding the highest mark, choosing the level, printing. That is four responsibilities in one function.
  2. Extract each computation into a pure function: average(marks), highest(marks) and level(avg). Each one returns its result instead of printing it.
  3. Use built-ins where they already do the job: sum() and max() replace the hand-written loops, so there is less code to test.
  4. Keep one output function, report(marks), that calls the others and prints. Its output must be exactly the same as before; that is what makes this a refactoring and not a rewrite.
  5. Now the decisions can be tested without reading the screen: assert level(80) == "Level 4" checks a boundary value directly.

The refactored program is below. It prints the same three lines, and the assert statements pass silently.

def average(marks):
"""Return the mean of a non-empty list of marks."""
return sum(marks) / len(marks)
def highest(marks):
"""Return the largest mark."""
return max(marks)
def level(avg):
"""Return the achievement-level text for an average."""
if avg >= 80:
return "Level 4"
if avg >= 70:
return "Level 3"
return "Below Level 3"
def report(marks):
"""Print the average, highest mark and level."""
avg = average(marks)
print("Average:", round(avg, 1))
print("Highest:", highest(marks))
print(level(avg))
report([84, 71, 90, 77])
assert level(80) == "Level 4"
assert level(79.9) == "Level 3"
assert average([70, 90]) == 80

After refactoring: three pure functions and one output function. The asserts produce no output when they pass.

Average: 80.5
Highest: 90
Level 4

Output of the program above (what Python prints).

Warning signWhat it suggestsFix
A function is longer than one screenIt probably has several jobsExtract helper functions
You copy and paste a block, changing one numberMissing parameterMake one function with that number as a parameter
The function name needs "and" (load_and_sort_and_print)Low cohesionSplit it into one function per job
A function reads or changes a global variableHigh couplingPass the value in and return the result
A calculation function calls print()Mixed computation and outputReturn the value; print it in the caller
Common signs that a decomposition needs work.
Just for fun

Why did the 400-line function go to therapy?

It had too many responsibilities and could not separate its work from its output.

Hands-on activity

Decompose a real task

You need: sticky notes (or small pieces of paper), a pen, and a computer with Python.

  1. Choose a task from home with several stages, such as planning a family pancake breakfast for 8 people (shopping list, scaling the recipe, cooking order, setting the table).
  2. Write the whole task on one note at the top of a table or wall. Below it, place one note per major step. Below each step, add notes for its smaller steps. Stop when each note is a job you could explain in one sentence without "and".
  3. Circle the notes that compute something (for example, scale 250 mL of milk for 8 people instead of 4) and underline the ones that need input or output (asking how many people, printing the list).
  4. Write Python stubs for the computing notes, for example def scale(amount, old_servings, new_servings): with return amount as a placeholder, plus a main() that calls them. Then fill in one stub and test it with an assert.

What to look for: a hierarchy at least three levels deep, function names that are verbs, and computing functions that return values instead of printing.

Your turn

1

In your own words, what is functional decomposition?

2

The function name read_and_validate_and_save_marks is a warning sign. Of what, and how would you fix it?

3

Which of these functions are pure? (a) def area(r): return 3.14159 * r ** 2 (b) def show(x): print(x) (c) def add_item(items, x): items.append(x)

4

Write format_time(seconds) from the lesson without looking. What does format_time(65) return?

5

Why should main() not contain a line like secs % 60?

6

Name two benefits of writing a function as a stub first.

7

Draw (in words) the structure chart for: ask the user for 5 temperatures, then print the warmest and the average.

More practice: warm-up

1

What does a line between two boxes in a structure chart mean?

2

Is high coupling good or bad?

3

Name one side effect a function can have.

4

What is a stub?

5

True or false: refactoring changes what a program outputs.

More practice: on your own

1

This code repeats itself:

print(round(12 * 1.13, 2))

print(round(30 * 1.13, 2))

print(round(7.5 * 1.13, 2))

Refactor it with a function. (In Ontario, HST is 13%.)

2

Why is def total(prices): return sum(prices) easier to test than def total(): print(sum(PRICES)) where PRICES is global?

3

Predict the output:

def double(n):

    return n * 2

def add_three(n):

    return n + 3

print(add_three(double(4)), double(add_three(4)))

4

A function process(data) reads a file, removes blank lines, sorts the lines and writes a new file. Decompose it into a hierarchy.

5

Give one reason not to decompose too far (for example, a separate function for every single line).

Challenge

1

Extend the race report so it also prints the median time. Which new function(s) do you add, and which existing functions change?

2

Explain why rank(results) returns a new sorted list (with sorted) instead of calling results.sort(). What could go wrong with .sort()?

3

A classmate says: "Decomposition is only about making code shorter." Give a counter-example where decomposition makes code longer but better.

Check your understanding

Quiz

1.

What is the main goal of functional decomposition?

2.

Which function best follows the single-responsibility principle?

3.

Which of these is a side effect?

4.

In a structure chart, where does main() usually appear?

5.

format_time(seconds) returns f"{seconds // 60}:{seconds % 60:02d}". What is format_time(304)?

6.

Why is low coupling desirable?

For parents and teachers

Big idea: Expectation C1.3 asks students to apply functional decomposition when designing subprograms, and C1.4 asks them to design for reuse. This lesson builds on the functions and structure charts from Grade 11 (ICS3U) and focuses on design judgment: where one function should end and the next begin.

How to help: Ask your teen to explain the race-report structure chart to you, box by box, without looking at the code. Then pick any household routine (laundry, packing for a trip) and ask them to decompose it out loud. If they can say what each piece receives and what it hands back, they understand interfaces.

Common difficulties:

  • Writing functions that print instead of return. Ask: "Could another program use this answer?"
  • Decomposing too little (one giant function) or too much (a function per line).
  • Changing the output while "refactoring". Encourage running the program before and after and comparing.
  • Code in practice answers uses special spaces for indentation; students should type it rather than copy it.