Decomposition and Modularisation
Decomposition is breaking a problem into smaller sub-problems that can be solved separately. Modularisation is the code that results: each sub-problem becomes a named module — in Python, a function.
The two connect directly to Module 1. Means-ends analysis said "find the biggest difference and reduce it, making preconditions into sub-goals". Decomposition is that same move, applied to a program's structure.
Why bother
The syllabus asks for "motivation for modularisation". Five reasons worth knowing:
1. You can only hold so much at once. A 200-line function must be understood all at once. Ten 20-line functions can be understood one at a time.
2. Reuse. A grade_for(marks) function written once serves the report
card, the statistics, and the pass list.
3. Testing. You can check is_prime(n) on its own. You cannot easily
check "the prime-checking bit in the middle of that long function".
4. Change is contained. If the grade boundaries change, one function changes. Logic scattered across a program means hunting every copy.
5. Division of labour. Two people can write two functions at once if the interface between them is agreed.
How to decompose
Ask: what are the distinct jobs here? Each answer is a candidate function. A good one:
- does one thing you can name in a short phrase
- has a clear input and output
- does not need to know how the others work
If naming a function takes a sentence with "and" in it, it is doing two things and should be two functions.
Example: merge sort
The syllabus names this. The decomposition is the algorithm:
sort a list
├── if it has 0 or 1 items, it is already sorted
└── otherwise
├── split into two halves
├── sort the left half (same problem, smaller)
├── sort the right half (same problem, smaller)
└── merge two sorted lists into one
Two sub-problems: split and merge. Sorting the halves is the same problem again — which is why merge sort is naturally recursive.
def merge(left, right):
"""Combine two already-sorted lists into one sorted list."""
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i]); i += 1
else:
result.append(right[j]); j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
def merge_sort(items):
"""Return a new sorted list."""
if len(items) <= 1:
return items[:] # base case
mid = len(items) // 2
return merge(merge_sort(items[:mid]), merge_sort(items[mid:]))
merge knows nothing about sorting; merge_sort knows nothing about how
merging works. Each can be read, tested and corrected alone.
Example: top three of n
The syllabus's second named example. The naive answer is "sort and take the last three" — and it is a perfectly good answer, because the decomposition is trivial. The interesting version is doing it in one pass:
find the top three
├── validate there are at least three
├── track the best three seen so far
└── for each item, insert it into that group if it beats the smallest
def top_three(numbers):
if len(numbers) < 3:
raise ValueError("need at least three numbers")
best = sorted(numbers[:3], reverse=True)
for x in numbers[3:]:
if x > best[2]:
best[2] = x
best.sort(reverse=True)
return best
Where decomposition stops
Do not decompose for its own sake. A function called once, three lines long, with a name no clearer than its body, adds a layer without adding understanding. Decompose when it makes something easier to name, reuse or test.