Traits, Generics, and Testing Discipline
Goal: you can define a trait contract, write two implementations plus a test fake,
and choose correctly between generics (impl Trait/bounds) and trait objects (dyn).
The trait is the contract
#![allow(unused)]
fn main() {
pub trait MatchStrategy {
fn matches(&self, haystack: &str, needle: &str) -> bool;
fn name(&self) -> &'static str;
}
}
This is the shape our production code uses for swap-able behavior: uteke’s index
traits, Sinau’s StorageBackend (local vs S3 behind one interface). Callers depend on
the contract, not an implementation — which is also what makes testing cheap: the
fake IS an implementation.
Static vs dynamic dispatch
| Style | Syntax | Choose when |
|---|---|---|
| Static (generics) | fn query<S: MatchStrategy>(s: &S) or SearchEngine<S> | strategy known at compile time; zero-cost, but one monomorphized copy per type |
| Dynamic (trait object) | fn query(docs: &[Document], s: &dyn MatchStrategy) or Box<dyn MatchStrategy> | strategy chosen at runtime (config, request param); one copy, tiny vtable hop |
The M3 lab’s engine is generic over S: MatchStrategy but main stores
Box<dyn MatchStrategy> selected from CLI args — both styles in one program, and the
tests exercise both.
Iterator chains over manual loops
#![allow(unused)]
fn main() {
self.docs.iter()
.filter(|d| s.matches(&d.body, q))
.map(|d| (d.id, score_of(d)))
.filter(|(_, score)| *score > 0)
.collect()
}
Reads top-to-bottom, composes, and lazily evaluates. Reach for itertools only when a
chain genuinely needs it; most filters/maps don’t.
Testing discipline (the house rules)
- Tests live in the same file:
#[cfg(test)] mod tests— they compile against private items, and nobody forgets to update them. - Arrange–Act–Assert, one behavior per test, name = the behavior
(
word_strategy_respects_boundaries). - Every public capability gets: a happy path, an empty/edge case, and a failure mode.
- A trait contract gets a conformance test per implementation — plus a fake for tests of code that depends on the trait.
Lab
labs/m3-search — three strategies (ExactSearch, CaseInsensitiveSearch,
WordSearch), a generic SearchEngine<S> with naive title-doubled ranking, and the
&dyn variant query_dyn. Queries: cargo run -p m3-search deploy ci.
Done when: cargo test -p m3-search is green (6 tests), and you can say when
dyn costs you anything (it doesn’t here) and why the fake-free design still tests
all strategies (conformance tests).