2.0 Why Structure Matters
Structure is the arrangement of code into packages, modules, and components. Good
structure makes a program easier to understand, test, and change. Poor structure
- everything entangled, no clear boundaries - makes every change risky and every
bug hard to isolate.
Two forces drive structural decisions:
-
Cohesion - code that belongs together should be packaged
together. A package that does one focused thing is easier to understand
and reuse than one that handles unrelated concerns.
-
Coupling - packages that must change together should be
few in number. High coupling means a change in one package ripples through
many others; low coupling means changes stay local.
The goal is high cohesion within packages and low coupling between them.
2.1 Packages and Components
A package is the unit of compilation and deployment. In Rust it is
a crate; in C++ a set of source and header files; in C# a project or assembly; in
Python a module or package directory.
A component is a logical unit within a package - typically a struct,
class, or set of functions that together implement one responsibility. A package may
contain one or several components.
Useful rules for package design:
- One primary responsibility per package.
- Public items form the interface; implementation details are private.
- Packages that change for the same reasons should be grouped; packages
that change for different reasons should be separated.
- Packages consumed by many others should be stable - resist the urge to
change their interfaces frequently.
2.2 Dependencies and Coupling
A dependency exists when one package uses types or functions defined
in another. Dependencies have direction: package A depends on package B if A calls
into B, but not the reverse.
Dependency diagrams show this structure visually. An arrow from A to B means A
depends on B. Healthy architectures have acyclic dependency graphs - no package
depends on itself transitively. Cycles are a design smell: they make independent
deployment and testing impossible.
TextFinder dependency graph (acyclic):
Executive -> DirNav -> (filesystem)
| -> TextSearch
| -> Display
| -> CmdlnParser
Coupling types - from tightest (most problematic) to loosest (preferred):
- Content coupling - one component directly modifies another's
internal data. Avoid completely.
- Common coupling - components share global state. Avoid;
makes concurrency and testing difficult.
- Control coupling - one component passes a flag telling
another what to do. Usually a sign of missing abstraction.
- Data coupling - components communicate only through
well-typed parameters and return values. This is the target.
- Message coupling - components communicate via events or
channels with no shared state. Best for concurrent systems.
2.3 Interfaces and Abstraction
An interface is the visible boundary of a component - the set of
functions, types, and constants that external callers can use. Everything else is
an implementation detail and should be hidden.
Good interfaces have three properties:
-
Narrow - expose only what callers need. Every public item is
a commitment you must maintain; keep the surface small.
-
Stable - changing an interface breaks all callers. Design
interfaces that can evolve without breaking existing code (add parameters with
defaults, use versioned names, or model behavior with traits and interfaces
rather than concrete types).
-
Abstract - interfaces should describe what a component
does, not how. A search function returns matched lines; callers do not
need to know whether it uses a regex engine, a finite automaton, or a simple
string scan.
In Rust, interfaces are expressed as traits. In C++ as abstract base classes or
template concepts. In C# as interfaces. In Python as duck-typed protocols or
abstract base classes. The syntax differs; the principle is identical.
2.4 Measuring Structure Quality
A few practical tests for whether a structure is healthy:
-
The one-sentence test: can you describe each package's
responsibility in one sentence without using "and"? If not, it likely
has too many responsibilities.
-
The change test: for a common type of change (add a
new search pattern, support a new file format), how many packages need
to be touched? The fewer the better.
-
The test isolation test: can you write a unit test for
each package without constructing the whole system? If not, coupling
is too tight somewhere.
-
The dependency cycle test: draw the dependency graph.
Are there cycles? Each cycle is a problem waiting to be felt.
2.5 References
Design Structure References