KTU S1

Decomposition and Modularisation

By the end you should be able to: Explain problem decomposition as a strategy, state the motivations for modularisation, and decompose a problem such as merge sort or "top three of n" into named sub-problems.

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.