3.0 What are Structural Patterns?
Structural patterns are recurring ways of organizing code into packages and components.
They are not frameworks or libraries - they are vocabulary for describing how
responsibilities are divided and how components communicate.
This chapter and the next cover five patterns that appear in the Design Bites sequence,
each illustrated with a line-counting program similar in scope to TextFinder:
- Monolithic (Basic) - one package, one struct
- Data Flow - components connected by typed event channels
- Factored - policy separated from mechanism
- Type Erasure - concrete types hidden behind trait objects (next chapter)
- Plugin - behavior loaded at runtime (next chapter)
No pattern is universally best. Each has tradeoffs between simplicity, flexibility,
testability, and runtime cost. Knowing all five gives you the vocabulary to choose
deliberately rather than by default.
3.1 Monolithic (Basic) Structure
In a monolithic structure all code lives in a single package. A single struct or
class holds the state and provides all the methods needed to read input, do the
work, and produce output.
Package: LineCounter
struct LineCounter {
root: path,
extensions: list<string>
}
impl LineCounter {
fn new(root, exts) -> Self
fn run() -> counts // walks dirs, opens files, counts lines
fn report(counts) // formats output
}
Monolithic - Pros and Cons
| Notes |
| Pros |
Simple to create and navigate; no inter-package plumbing; one piece to track
and deploy.
|
| Cons |
All concerns are mixed together; difficult to unit-test individual parts;
adding a new output format or traversal strategy requires editing the same
struct; grows harder to understand as the codebase expands.
|
| Best for |
Small, short-lived scripts or proofs of concept where simplicity is the only
priority.
|
3.2 Data Flow Structure
A data flow structure decomposes the program into components connected by typed
channels or event callbacks. Data flows from one component to the next; each
component has no knowledge of its neighbors' implementations.
Packages: DirNav, TextProcessor, Collector
DirNav discovers files and fires a FileEvent for each.
TextProcessor receives FileEvents, opens files, emits LineEvents.
Collector receives LineEvents and accumulates counts.
Data flow: DirNav --FileEvent--> TextProcessor --LineEvent--> Collector
The event types are the contracts between stages. Each stage only depends on the
event type, not on the stage that produces it. You can swap DirNav for a network
source without changing TextProcessor.
Data Flow - Pros and Cons
| Notes |
| Pros |
Components are decoupled at the event boundary; easy to insert new stages
or replace existing ones; natural fit for concurrent pipelines.
|
| Cons |
More indirection than monolithic; event types must be designed carefully
to carry enough context; debugging a broken pipeline requires tracing
events across stages.
|
| Best for |
Processing pipelines, streaming data, and systems where stages need to
evolve independently or run concurrently.
|
3.3 Factored Structure
A factored structure separates policy from mechanism. The policy
decides what to do and in what order. The mechanisms implement the individual
operations. The policy component depends on abstract interfaces, not on concrete
implementations.
Packages: Executive (policy), DirNav, FileReader, Counter (mechanisms)
Executive holds references to abstract traits:
trait Traverser { fn walk(root) -> iter<path>; }
trait Reader { fn read(path) -> iter<line>; }
trait Accumulator { fn add(line); fn total() -> usize; }
Executive::run() {
for path in self.traverser.walk(root) {
for line in self.reader.read(path) {
self.accumulator.add(line);
}
}
}
Concrete types (DirNav, FileReader, Counter) implement the traits. Executive
never imports them directly. To substitute a mock reader for testing, inject
a different implementation of Reader.
Factored - Pros and Cons
| Notes |
| Pros |
Policy and mechanisms are independently testable and replaceable; adding
a new traversal strategy (e.g., zip file search) requires only a new
implementation of Traverser, not changes to Executive.
|
| Cons |
More upfront design effort; introduces trait objects or abstract base classes;
slight indirection cost at each interface call.
|
| Best for |
Systems where the core algorithm is stable but the implementations of its
steps are expected to vary or grow over time.
|
3.4 References
Structural Patterns References