Unit 06 · lesson

Decompose by Responsibility and Test the Pieces

A longer program should not become a long main method merely because it still runs.

Suppose the requirement is:

Given completed and total tasks, reject impossible counts, calculate a percentage, classify progress, and format a report line.

That requirement contains multiple responsibilities.

One decomposition:

isValidCounts(completed, total)
calculatePercent(completed, total)
classifyPercent(percent)
formatReport(completed, total, percent, label)
main() coordinates the flow

Notice what is not happening: one method per line of code. Decomposition follows meaning.

Build from contracts

Before implementation, define each method.

isValidCounts
inputs: completed, total
returns: boolean
rules: total > 0; completed >= 0; completed <= total
calculatePercent
inputs: valid completed, total
returns: percentage as double
precondition: counts already valid
classifyPercent
input: 0..100 percentage
returns: one of "LOW", "ON TRACK", "COMPLETE"

Contracts expose dependencies. calculatePercent should not quietly invent behavior for impossible counts if validation owns that concern.

Integration can fail even when pieces pass

Imagine each method works separately, but main calls calculatePercent(total, completed) with arguments reversed.

The unit-level tests may pass. Integration fails because the collaboration is wrong.

That is why testing methods individually does not eliminate the need for end-to-end cases.

Test the boundaries of each responsibility

For isValidCounts, useful cases include:

completedtotalexpected
01true
11true
-11false
21false
00false

For calculatePercent, do not test invalid counts if the contract says it requires validated inputs. Instead test numeric boundaries like 0/4, 1/4, and 4/4.

The test set should match the method's responsibility.

Refactor without changing behavior

Start with a working single-method version of a small program. Capture expected outputs for three inputs.

Then extract at least two meaningful methods.

Run the same inputs again.

If output changes, one of two things happened:

  • you intentionally changed a requirement;
  • the refactor changed behavior accidentally.

A refactor is supposed to improve structure without changing required behavior.

Explain the call graph

Draw arrows showing which methods call which:

main
 |-- isValidCounts
 |-- calculatePercent
 |-- classifyPercent
 `-- formatReport

Then identify which methods could be tested without input/output side effects.

Evidence

Submit:

  • the original requirement;
  • your method contracts;
  • a call graph;
  • at least one boundary test per major method;
  • three before/after outputs showing a refactor preserved behavior;
  • one sentence explaining a decomposition choice you rejected.

Software design begins when you can explain why the pieces exist, not when the file contains many methods.