SWDev Story

SWDev Story: Advanced Patterns

type erasure, plugin, choosing a pattern

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
PatternChoose 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
ResourceDescription
Design Bites: Type Erasure Type erasure walkthrough with code in Rust and C++.
Design Bites: Plugin Plugin structure with dynamic loading examples.
Rust Book - Trait Objects Official Rust explanation of trait objects and dynamic dispatch.
Refactoring Guru - Strategy Pattern The Strategy pattern - closely related to factored and type-erased structures.