4.0 Advanced Structural Patterns
The two patterns in this chapter - type erasure and plugin - build on the factored
structure from the previous chapter. Both extend the concept of hiding concrete types
behind abstractions, but do so in different ways and at different points in a
program's lifecycle.
Type erasure hides concrete types at compile time, storing heterogeneous
implementations through a common interface. Plugin structure goes further: it
defers the choice of implementation until runtime, loading behavior from external
shared libraries or modules.
4.1 Type Erasure
Type erasure is the technique of storing objects of different concrete types through
a single abstract interface, so the caller never needs to know what concrete type
it holds.
In Rust this is done with Box<dyn Trait> or
Arc<dyn Trait>. In C++ with pointers or references to abstract base
classes. In C# with interface references. In Python, duck typing provides it
naturally.
// Rust example - type erasure with trait objects
trait Processor {
fn process(&self, path: &Path) -> Vec<String>;
}
struct LineCounter;
struct WordCounter;
impl Processor for LineCounter { ... }
impl Processor for WordCounter { ... }
// callers work only with Box<dyn Processor>
fn run_all(processors: &[Box<dyn Processor>], path: &Path) {
for p in processors {
let results = p.process(path);
// ...
}
}
The caller holds a collection of Box<dyn Processor> and can iterate
over them without knowing their concrete types. New processors can be added without
changing the calling code.
Type Erasure - Pros and Cons
| Notes |
| Pros |
Heterogeneous collections without templates or enums; callers are fully
decoupled from concrete implementations; easy to inject mocks for testing.
|
| Cons |
Virtual dispatch overhead (small but real); in Rust, Box allocation per
object; loss of type information makes some optimizations harder.
|
| Best for |
Collections of related but distinct implementations - e.g., a list of
document processors, rendering backends, or transformation passes.
|
4.2 Plugin Structure
A plugin structure takes type erasure one step further: the concrete implementations
are not compiled into the main binary. They are loaded at runtime from shared
libraries (.so on Linux, .dll on Windows) or from Python
modules discovered on disk.
The host application defines and publishes the plugin interface - a trait, abstract
class, or protocol. Each plugin is a separate artifact that implements the interface.
At startup the host scans a plugin directory, loads each artifact, and calls a
registration function to obtain an implementation of the interface.
Plugin architecture for TextFinder:
Host binary defines:
trait OutputPlugin { fn write(&self, match: Match); }
host loads plugins at startup:
- plugins/json_output.so -> implements OutputPlugin
- plugins/csv_output.so -> implements OutputPlugin
- plugins/html_output.so -> implements OutputPlugin
Host calls each plugin through the trait interface;
concrete types are never visible to the host.
Plugin - Pros and Cons
| Notes |
| Pros |
New behavior can be added without recompiling the host; users or third
parties can contribute plugins; the host binary stays small; different
plugins can be built with different toolchains.
|
| Cons |
Significant additional complexity - dynamic loading, ABI stability across
compiler versions, error handling for missing or corrupt plugins; security
implications if plugins come from untrusted sources.
|
| Best for |
Extensible applications where end users or third parties add capabilities -
editors, media players, IDEs, rendering engines.
|
4.3 Choosing a Pattern
The five structural patterns form a progression from simplest to most flexible:
Pattern Selection Guide
| Pattern | Choose when... | Avoid when... |
| Monolithic |
Small, short-lived program; simplicity is the only goal. |
Code will grow, be tested in parts, or be reused. |
| Data Flow |
Processing naturally decomposes into stages; concurrent execution is desired. |
Stages are tightly interdependent or event contracts are expensive to define. |
| Factored |
Core algorithm is stable; implementations of steps are expected to vary. |
Abstractions are premature - the variation hasn't appeared yet. |
| Type Erasure |
Heterogeneous collection of implementations must be treated uniformly. |
All types are known at compile time and a generic (static dispatch) works. |
| Plugin |
New behavior must be loadable without recompiling the host. |
ABI stability is not feasible or the added complexity is not justified. |
Start simple. Use a monolithic structure until complexity forces a split. Move to
data flow when stages need to evolve independently. Introduce factoring when a
policy/mechanism separation clarifies the design. Add type erasure when you need
heterogeneous collections. Reserve plugins for systems that genuinely need
runtime extensibility.
4.4 References
Advanced Patterns References