Enums vs Traits vs Generics vs Trait Objects
Rule
Use enums for closed sets, traits for open extension points, generics for static dispatch, and dyn Trait for runtime heterogeneity.
Why
Each choice encodes a different extension and dispatch model, so the API should make that model deliberate.
Do
- Use an enum when this crate controls every variant.
- Put shared behavior on a closed enum when callers should not extend it.
- Use a trait when callers or another layer must supply new behavior.
- Use
impl Traitor a type parameter when one implementor type flows through the call. - Use trait objects for runtime selection or heterogeneous collections.
- Prefer returning concrete types or
impl Traitunless runtime polymorphism is required. - Follow trait design for contracts, object safety, and sealing.
Avoid
- Do not create a trait only because closed variants share method names.
- Do not use a closed enum when downstream users must add variants.
- Do not spread generics through many layers when a trait object localizes the choice.
- Do not use dynamic dispatch merely to avoid a type parameter.
- Do not expose a trait-object API before confirming the trait is object-safe.
Public API Notes
Adding an enum variant can break exhaustive downstream matches. An open public trait commits the crate to supporting external implementors.
Example
#![allow(unused)]
fn main() {
pub enum DeliveryTarget {
Email(EmailAddress),
Webhook(WebhookUrl),
}
pub trait Notifier {
fn notify(&self, message: &Message) -> Result<(), NotifyError>;
}
pub fn notify_once(notifier: &impl Notifier, message: &Message) -> Result<(), NotifyError> {
notifier.notify(message)
}
pub struct Broadcast {
notifiers: Vec<Box<dyn Notifier>>,
}
}
Exceptions
- Use a trait for a closed set when existing generic infrastructure requires it.
- Use an enum wrapper around trait objects when the public categories are closed but implementations are dynamic.
- Use generics publicly only when caller flexibility outweighs type complexity.