Rules of thumb for turning a first-cut structure into a good one.
π Where this lives: these heuristics are what a senior engineer applies in a code review without being able to name them. "Why does this function take a boolean flag?" is fan-in/control-coupling. "Why does the UI layer know about the database schema?" is a layering violation. Modern static-analysis tools β SonarQube, CodeClimate, the complexity gates in CI β automate a subset of exactly this list, which is why a pull request can now be rejected by a machine for reasons Yourdon and Constantine wrote down in 1979. Search "cyclomatic complexity threshold code quality gate".
The classical heuristics
Once a program structure has been developed using functional
decomposition, effective modularity can be achieved by
manipulating it according to the following heuristics.
1. EVALUATE THE FIRST ITERATION OF THE PROGRAM STRUCTURE TO
REDUCE COUPLING AND IMPROVE COHESION.
Once developed, modules may be EXPLODED or IMPLODED.
EXPLODE β a module performing several tasks is split
into more, single-purpose modules. Do this
when cohesion is low.
IMPLODE β several modules whose processing occurs in a
fixed sequence and whose data is shared are
combined. Do this when the split created
coupling without buying independence.
The guiding rule: STRIVE FOR FUNCTIONAL INDEPENDENCE.
Modules with high cohesion and low coupling should be the
goal.
2. ATTEMPT TO MINIMISE STRUCTURES WITH HIGH FAN-OUT; STRIVE
FOR FAN-IN AS DEPTH INCREASES.
FAN-OUT the number of modules directly controlled by a
module. High fan-out (say > 7) suggests the
module is a dispatcher doing too much
coordination β the "God controller".
FAN-IN the number of modules that directly call a
module. High fan-in low in the hierarchy is
GOOD: it means a genuinely reusable utility.
The healthy shape is therefore not a flat wide tree nor a
narrow deep one, but an OVAL β narrow at the top, wide in
the middle, converging on shared services at the bottom.
3. KEEP THE SCOPE OF EFFECT OF A MODULE WITHIN THE SCOPE OF
CONTROL OF THAT MODULE.
SCOPE OF CONTROL of module m = m itself plus all modules
subordinate to it.
SCOPE OF EFFECT of a decision made in m = all modules
affected by that decision.
THE RULE: if a decision made in m affects a module NOT
subordinate to m, the structure is wrong β because the
affected module cannot be reasoned about locally. Either
move the decision up, or restructure.
4. EVALUATE MODULE INTERFACES TO REDUCE COMPLEXITY AND
REDUNDANCY AND IMPROVE CONSISTENCY.
A complex interface is a prime source of errors. It should
communicate simple information and be consistent with the
module's function β an interface that passes data
unrelated to what the module does signals a design error.
5. DEFINE MODULES WHOSE FUNCTION IS PREDICTABLE, BUT AVOID
MODULES THAT ARE OVERLY RESTRICTIVE.
PREDICTABLE (a "black box"): the same input always yields
the same result, with no side effects, and the caller need
not know the internals. A module that maintains hidden
internal state across calls is unpredictable and
untrustworthy.
BUT: a module restricted to a single narrow case (a table
size hard-coded, a currency assumed) forces the caller to
work around it. Parameterise what genuinely varies.
6. STRIVE FOR CONTROLLED ENTRY MODULES, AVOIDING
"PATHOLOGICAL CONNECTIONS".
Software should be entered at its top β ONE entry, ONE
exit. A pathological connection is a branch or reference
into the MIDDLE of a module. It makes the module
impossible to reason about and impossible to change
safely. (Content coupling, in the coupling ladder.)
7. PACKAGE SOFTWARE BASED ON DESIGN CONSTRAINTS AND
PORTABILITY REQUIREMENTS.
Packaging = the assembly of software into physical
modules. Where memory is constrained, or platform
portability matters, package so that the constrained or
platform-specific parts are isolated. Requirements may
dictate that a program overlay itself, or that modules be
separately loadable.
Worked: applying the heuristics to a bad structure
A FIRST-CUT DESIGN for the licence system's reporting feature:
ReportController
βββ fetchApplications()
βββ fetchOfficers()
βββ fetchFees()
βββ computeTotals()
βββ formatAsPdf()
βββ formatAsExcel()
βββ emailReport()
βββ logReportRun()
βββ updateDashboardCache()
DIAGNOSIS, heuristic by heuristic:
H2 FAN-OUT = 9. Well above the ~7 guideline. This is a
dispatcher, and every new report format or delivery
channel widens it further.
H1 COHESION of ReportController is TEMPORAL at best β these
nine things are related only by "happen during a report
run". Data access, computation, formatting, delivery and
caching are four different concerns.
H4 INTERFACE COMPLEXITY: computeTotals() must receive
applications, officers AND fees β a large parameter list
signalling that the data-gathering concern has leaked into
the computation concern.
H3 SCOPE OF EFFECT: updateDashboardCache() means a decision
made inside the report run affects the dashboard, which is
not subordinate to ReportController. A report should not be
able to break a dashboard.
RESTRUCTURED, applying EXPLODE and then IMPLODE:
ReportService (fan-out 3)
βββ ReportDataGateway β implodes the three
β fetchReportData(spec) fetch* modules; they
β ran in fixed sequence
β and shared data (H1)
βββ ReportCalculator β pure function:
β totals(data) β Totals predictable, no side
β effects (H5)
βββ ReportRenderer β explodes into a strategy
render(totals, format) interface; PdfRenderer
and ExcelRenderer are
siblings with HIGH FAN-IN
from any caller (H2)
Delivery and cache-invalidation move OUT, triggered by a
ReportGenerated event β so a report no longer reaches into the
dashboard (H3), and adding an SMS channel touches nothing in
ReportService.
MEASURED IMPROVEMENT:
fan-out 9 β 3
cohesion temporal β functional (each new module
has one task)
coupling stamp/control β data + message
new output format edit ReportController β add one class
testability needs a DB, a mailer and a cache β
ReportCalculator is a pure function
testable with no infrastructure at all
THAT LAST LINE IS THE PRIZE. The heuristics are not
aesthetics; the restructured design has a piece you can unit
test in microseconds, and the original did not.
Complexity, quantified
Heuristics are qualitative. Two measures make them checkable.
CYCLOMATIC COMPLEXITY (McCabe) β the number of linearly
independent paths through a module.
V(G) = E β N + 2P
E = edges in the control-flow graph
N = nodes
P = connected components (1 for a single module)
Equivalently, for a single module:
V(G) = (number of binary decision points) + 1
EXAMPLE β a fee validator:
if (amount <= 0) return ERR_NEGATIVE;
if (amount > MAX_FEE) return ERR_TOO_LARGE;
if (district == null) return ERR_NO_DISTRICT;
if (isHoliday(date) && !allowHolidayPayment)
return ERR_HOLIDAY;
return OK;
decision points: 3 simple ifs + 1 if with && (which is 2
decisions) = 5
V(G) = 5 + 1 = 6
β 6 independent paths, so a minimum of 6 test cases for
full path coverage
INTERPRETATION (the widely used bands):
1β10 simple, low risk
11β20 moderate complexity, moderate risk
21β50 complex, high risk
> 50 untestable, very high risk
V(G) is ALSO A LOWER BOUND ON TEST CASES for basis-path
coverage, which is what makes it useful rather than merely
descriptive.
STRUCTURAL COMPLEXITY / COUPLING METRIC:
Structural complexity of module i:
S(i) = f_out(i)Β²
β complexity grows as the SQUARE of fan-out, which is the
quantitative justification for heuristic 2.
Data complexity:
D(i) = v(i) / (f_out(i) + 1)
where v(i) is the number of input and output variables
passed to and from module i.
System complexity: C(i) = S(i) + D(i)
As each of these increases, the overall architectural
complexity increases, leading to greater integration and
testing effort.
APPLIED TO THE EXAMPLE ABOVE:
before: S = 9Β² = 81
after: S = 3Β² = 9
A 9-fold reduction in structural complexity from one
restructuring β and that is exactly what heuristic 2 is
claiming when it says "minimise fan-out".
MORPHOLOGY measures for a program structure with n nodes and a
arcs:
size = n + a
depth = longest path root β leaf
width = maximum number of nodes at any one level
arc-to-node ratio r = a / n β a measure of coupling
A high r means many connections per module, i.e. high
coupling; comparing r across candidate designs is a cheap
way to choose between them.
Heuristic 3 is the one most often skipped and most often violated in real code. "A decision made here affects something not underneath me" is precisely what makes a codebase feel haunted β you change a report and a dashboard breaks. Keeping scope of effect inside scope of control is what makes local reasoning possible.
π Go further: the modern descendant of heuristic 5 ("predictable modules, no hidden state") is referential transparency β a function whose output depends only on its inputs can be cached, retried, parallelised, and tested without a fixture. That is why the restructured example's ReportCalculator is the valuable piece: functional-programming practice takes heuristic 5 and makes it the default rather than the goal, which is also why property-based testing tools (QuickCheck, Hypothesis) work on pure functions and are nearly useless on stateful ones. Search "referential transparency pure function testability".
π‘ Exam angle: list the seven design heuristics β they are frequently asked as a straight enumeration, so learn them in order. Define fan-in and fan-out and state the rule (minimise fan-out, strive for fan-in as depth increases). Define scope of control and scope of effect and state the containment rule. Explain explode and implode. Compute cyclomatic complexity from a code fragment using V(G) = E β N + 2P or decisions + 1, and give the risk bands β this is an almost-certain numerical question.
Syllabus points
Design heuristics for effective modularity
Create a free account to tick topics off, take notes as you read, watch the video lessons and get a day-by-day study plan built around your exam date.