A function can accept a trait reference so callers provide behavior without exposing concrete types.

Program

Play the program to choose a status code and format it through a trait boundary.

trait_formatter_boundary.rs
trait Formatter {
    fn status(&self, code: u16) -> String;
}

struct PlainFormatter;

impl Formatter for PlainFormatter {
    fn status(&self, code: u16) -> String {
        format!("status={code}")
    }
}

fn render(formatter: &dyn Formatter, code: u16) -> String {
    formatter.status(code)
}

fn main() {
    let code = ;
    let formatter = PlainFormatter;
    let text = render(&formatter, code);
    println!("{text}");
}
trait Formatter {
    fn status(&self, code: u16) -> String;
}

struct PlainFormatter;

impl Formatter for PlainFormatter {
    fn status(&self, code: u16) -> String {
        format!("status={code}")
    }
}

fn render(formatter: &dyn Formatter, code: u16) -> String {
    formatter.status(code)
}

fn main() {
    let code = ;
    let formatter = PlainFormatter;
    let text = render(&formatter, code);
    println!("{text}");
}
trait Formatter {
    fn status(&self, code: u16) -> String;
}

struct PlainFormatter;

impl Formatter for PlainFormatter {
    fn status(&self, code: u16) -> String {
        format!("status={code}")
    }
}

fn render(formatter: &dyn Formatter, code: u16) -> String {
    formatter.status(code)
}

fn main() {
    let code = ;
    let formatter = PlainFormatter;
    let text = render(&formatter, code);
    println!("{text}");
}
trait `Formatter` describes behavior without naming a concrete implementation.
trait object `&dyn Formatter` accepts any formatter behind a shared reference.
boundary `render` depends on formatting behavior, not on the `PlainFormatter` type.