Advanced Rust Design Patterns

Extensible Trait-Based APIs

Designing extensible trait-based APIs allows a library to add new convenience methods to existing types (Extension Traits) while controlling whether external downstream crates are permitted to implement a trait (Sealed Traits).

Two Complementary Trait Design Patterns:

  1. Extension Trait Pattern (`pub trait SliceStatsExt: Sealed`):
     Adds `.median()`, `.variance()` methods directly onto `[f64]` or `Iterator`!
     Callers simply write `use my_crate::SliceStatsExt;` to unlock the methods.

  2. Sealed Trait Pattern (`mod private { pub trait Sealed {} }`):
     Prevents external crates from implementing `pub trait ByteOrder: private::Sealed`
     so adding new required methods to `ByteOrder` in a minor release is NEVER a breaking change!

1. The Extension Trait Pattern (*Ext)

Rust's coherence rules (the Orphan Rule) prevent a library from adding inherent methods (impl<T> [T] { ... }) directly onto foreign types like [T], Iterator, Read, or Future.

Instead, define an Extension Trait (named *Ext by convention, such as Itertools, ReadBytesExt, or FutureExt) and provide a blanket implementation (impl<I: Iterator> MyIterExt for I):

/// Extension trait that adds a `.tap_each(...)` inspection adapter to every `Iterator`!
pub trait TapIteratorExt: Iterator + Sized {
    fn tap_count(self, counter: &mut usize) -> TapCount<Self> {
        TapCount {
            inner: self,
            counter,
        }
    }
}

/// Blanket implementation: every type implementing `Iterator` automatically gets `.tap_count()`!
impl<I: Iterator> TapIteratorExt for I {}

pub struct TapCount<'a, I> {
    inner: I,
    counter: &'a mut usize,
}

impl<I: Iterator> Iterator for TapCount<'_, I> {
    type Item = I::Item;

    fn next(&mut self) -> Option<Self::Item> {
        let item = self.inner.next()?;
        *self.counter += 1;
        Some(item)
    }
}

2. The Sealed Trait Pattern (Preventing External Implementations)

Normally, if you publish a pub trait FormatCodec, any downstream crate can write impl FormatCodec for TheirType. That means adding a new required method to FormatCodec later is a SemVer breaking change!

When a public trait is intended only to be implemented by types inside your own crate (for example, typestate markers, primitive integer bounds like u8/u16/u32/u64, or indexing overloads like std::slice::SliceIndex), seal the trait by giving it a supertrait private::Sealed located inside a private module:

  • Because pub trait Sealed sits inside a private mod private, external crates cannot name private::Sealed and therefore cannot implement your sealed trait!
  • Because external crates cannot implement the trait, you can add new methods to the trait in future minor releases (1.1.0 -> 1.2.0) without breaking SemVer!
mod private {
    /// Supertrait placed inside a private module: external crates cannot name or implement it!
    pub trait Sealed {}
    impl Sealed for u16 {}
    impl Sealed for u32 {}
    impl Sealed for u64 {}
}

/// A public trait that callers can use as a generic bound (`T: WireUnsigned`),
/// but that ONLY `u16`, `u32`, and `u64` can ever implement!
pub trait WireUnsigned: private::Sealed + Copy {
    fn byte_width() -> usize;
    fn to_u64(self) -> u64;
}

impl WireUnsigned for u16 {
    fn byte_width() -> usize { 2 }
    fn to_u64(self) -> u64 { u64::from(self) }
}

impl WireUnsigned for u32 {
    fn byte_width() -> usize { 4 }
    fn to_u64(self) -> u64 { u64::from(self) }
}

impl WireUnsigned for u64 {
    fn byte_width() -> usize { 8 }
    fn to_u64(self) -> u64 { self }
}

pub fn describe_wire_int<T: WireUnsigned>(val: T) -> String {
    format!("{} ({} bytes)", val.to_u64(), T::byte_width())
}

fn main() {
    // 1. Test Extension Trait (`TapIteratorExt`):
    let mut visited = 0usize;
    let sum: i32 = [10, 20, 30, 40]
        .into_iter()
        .tap_count(&mut visited)
        .take(3)
        .sum();
    assert_eq!(sum, 60);
    assert_eq!(visited, 3);

    // 2. Test Sealed Trait (`WireUnsigned`):
    println!(
        "Visited {} items (sum={sum}); WireUnsigned u32: {}",
        visited,
        describe_wire_int(42u32)
    );
}
Visited 3 items (sum=60); WireUnsigned u32: 42 (4 bytes)