Lesson 61 of 94
Creational Patterns
Many creational patterns exist to work around the limits of constructors. A constructor must be called with new, always returns a fresh object of exactly its class, has no name of its own, and can only fail by throwing. Patterns such as static factory methods, factories, and builders are ways around those limits.
Rust has no constructors. A value is created with a struct literal, and anything more is an ordinary function that returns the value. So a function can have a descriptive name, return a Result, return a cached value, or return a different type behind a trait, without a pattern. What remains of the creational patterns is about deciding which type to create at runtime, assembling values with many options, copying templates, and wiring objects together.
| Pattern | Rust |
|---|---|
| constructor overloads | named associated functions: new, with_capacity, from_hex |
| telescoping constructors | Default + struct update syntax ..Default::default() |
| Singleton | static + LazyLock or OnceLock, or better, a parameter |
| Factory method | a function returning Box<dyn Trait>, or an enum |
| Factory as a value | a closure, impl Fn() -> T |
| Abstract factory | a trait with associated types, or methods returning trait objects |
| Builder | a builder struct, consuming (self) or borrowing (&mut self) |
| Prototype | #[derive(Clone)]; clone_box for trait objects |
| Dependency injection | constructor parameters, wired by hand in main |
Part 1: Constructors are just functions
1. Named constructors
Static factory methods are often preferred over constructors: they have names, can validate, and can return shared instances. In Rust, that's the only way to construct anything beyond a struct literal, so it's simply how Rust code looks.
#[derive(Debug, Clone, Copy, PartialEq)]
struct Color {
r: u8,
g: u8,
b: u8,
}
impl Color {
const BLACK: Color = Color { r: 0, g: 0, b: 0 };
fn rgb(r: u8, g: u8, b: u8) -> Color {
Color { r, g, b }
}
fn gray(level: u8) -> Color {
Color { r: level, g: level, b: level }
}
fn from_hex(hex: &str) -> Result<Color, String> {
let digits = hex.strip_prefix('#').ok_or_else(|| format!("{hex:?} must start with #"))?;
if digits.len() != 6 {
return Err(format!("{hex:?} must have 6 hex digits"));
}
let channel = |i: usize| u8::from_str_radix(&digits[i..i + 2], 16).map_err(|e| format!("{hex:?}: {e}"));
Ok(Color { r: channel(0)?, g: channel(2)?, b: channel(4)? })
}
}
fn main() {
println!("{:?}", Color::rgb(255, 128, 0));
println!("{:?}", Color::gray(200));
println!("{:?}", Color::from_hex("#1e90ff"));
println!("{:?}", Color::from_hex("1e90ff"));
println!("{:?}", Color::from_hex("#12345z"));
println!("black is gray(0): {}", Color::gray(0) == Color::BLACK);
}Color { r: 255, g: 128, b: 0 }
Color { r: 200, g: 200, b: 200 }
Ok(Color { r: 30, g: 144, b: 255 })
Err("\"1e90ff\" must start with #")
Err("\"#12345z\": invalid digit found in string")
black is gray(0): trueThree constructors with three different meanings, and one of them can fail.
2. Default instead of telescoping constructors
Types with many optional settings often grow a ladder of constructors, each adding one parameter. In Rust, implement Default once and override only what differs with struct update syntax.
#[derive(Debug)]
struct ServerConfig {
host: String,
port: u16,
workers: usize,
tls: bool,
request_timeout_secs: u64,
}
impl Default for ServerConfig {
fn default() -> Self {
ServerConfig { host: String::from("127.0.0.1"), port: 8080, workers: 4, tls: false, request_timeout_secs: 30 }
}
}
fn main() {
let dev = ServerConfig::default();
let prod = ServerConfig { host: String::from("0.0.0.0"), tls: true, workers: 16, ..ServerConfig::default() };
println!("{dev:?}");
println!("{prod:?}");
}ServerConfig { host: "127.0.0.1", port: 8080, workers: 4, tls: false, request_timeout_secs: 30 }
ServerConfig { host: "0.0.0.0", port: 8080, workers: 16, tls: true, request_timeout_secs: 30 }When the "zero" value of every field is the right default (0, false, empty strings and collections), #[derive(Default)] writes the implementation for you.
Part 2: Singleton
3. A lazily initialized global
A singleton is a single global instance. In Rust, a static with LazyLock gives you a thread-safe global, created the first time it's used.
use std::collections::HashMap;
use std::sync::LazyLock;
struct FeatureFlags {
flags: HashMap<&'static str, bool>,
}
impl FeatureFlags {
fn is_enabled(&self, name: &str) -> bool {
self.flags.get(name).copied().unwrap_or(false)
}
}
static FLAGS: LazyLock<FeatureFlags> = LazyLock::new(|| {
println!("(loading feature flags once)");
FeatureFlags { flags: HashMap::from([("new_checkout", true), ("dark_mode", false)]) }
});
fn main() {
println!("new_checkout: {}", FLAGS.is_enabled("new_checkout"));
println!("dark_mode: {}", FLAGS.is_enabled("dark_mode"));
println!("unknown: {}", FLAGS.is_enabled("beta"));
}(loading feature flags once)
new_checkout: true
dark_mode: false
unknown: false4. A global set once at startup
LazyLock computes its value from code written in the program. When the value comes from runtime input, such as command-line arguments or a config file, use OnceLock: set it once during startup, then read it from anywhere. A second set returns an error instead of silently replacing the value.
use std::sync::OnceLock;
struct Settings {
region: String,
max_upload_mb: u32,
}
static SETTINGS: OnceLock<Settings> = OnceLock::new();
fn init_settings(args: &[&str]) {
let region = args.first().copied().unwrap_or("eu-west-1").to_string();
if SETTINGS.set(Settings { region, max_upload_mb: 25 }).is_err() {
println!("(settings were already initialized; ignoring)");
}
}
fn settings() -> &'static Settings {
SETTINGS.get().expect("settings not initialized: call init_settings at startup")
}
fn upload_limit_message() -> String {
let s = settings();
format!("uploads in {} are limited to {} MB", s.region, s.max_upload_mb)
}
fn main() {
init_settings(&["us-east-2"]);
println!("{}", upload_limit_message());
init_settings(&["ap-south-1"]); // can't replace it
println!("{}", upload_limit_message());
}uploads in us-east-2 are limited to 25 MB
(settings were already initialized; ignoring)
uploads in us-east-2 are limited to 25 MBBoth types are thread-safe. Their single-threaded counterparts, LazyCell and OnceCell, were covered in Cell & RefCell.
5. Often better: pass it as a parameter
A singleton is a hidden dependency: nothing in price_label's signature would tell you it reads global settings, and a test can't give it different ones. The fix is to pass a reference; it costs nothing at runtime.
struct Config {
currency: &'static str,
tax_percent: u32,
}
// The dependency is visible in the signature, and tests can pass any config.
fn price_label(cents: u32, config: &Config) -> String {
let total = cents + cents * config.tax_percent / 100;
format!("{}.{:02} {}", total / 100, total % 100, config.currency)
}
fn main() {
let germany = Config { currency: "EUR", tax_percent: 19 };
let test_config = Config { currency: "XXX", tax_percent: 0 };
println!("{}", price_label(1_000, &germany));
println!("{}", price_label(1_000, &test_config));
}11.90 EUR
10.00 XXXKeep globals for things that really are global and read-only, like feature flags, compiled regular expressions, or lookup tables. When a program has several shared services, group them in one context struct and pass that (example 19).
Part 3: Factories
6. Factory method
A factory decides which concrete type to create. Return Box<dyn Trait> when callers only need the interface.
trait Storage {
fn save(&mut self, key: &str, value: &str) -> String;
}
struct MemoryStorage(Vec<(String, String)>);
struct FileStorage {
dir: String,
}
impl Storage for MemoryStorage {
fn save(&mut self, key: &str, value: &str) -> String {
self.0.push((key.into(), value.into()));
format!("stored {key} in memory ({} entries)", self.0.len())
}
}
impl Storage for FileStorage {
fn save(&mut self, key: &str, _value: &str) -> String {
format!("wrote {}/{key}.json", self.dir)
}
}
fn storage_for(env: &str) -> Result<Box<dyn Storage>, String> {
match env {
"test" => Ok(Box::new(MemoryStorage(Vec::new()))),
"prod" => Ok(Box::new(FileStorage { dir: String::from("/var/lib/app") })),
other => Err(format!("unknown environment {other:?}")),
}
}
fn main() {
for env in ["test", "prod", "staging"] {
match storage_for(env) {
Ok(mut s) => println!("{env}: {}", s.save("user-1", "{}")),
Err(e) => println!("{env}: {e}"),
}
}
}test: stored user-1 in memory (1 entries)
prod: wrote /var/lib/app/user-1.json
staging: unknown environment "staging"7. A factory that returns an enum
When you own all the variants, return an enum instead of a trait object: no allocation, and callers can match on the result. Implementing FromStr turns the factory into .parse(), the same method that parses numbers.
use std::str::FromStr;
#[derive(Debug)]
enum Shape {
Circle { radius: f64 },
Square { side: f64 },
Rect { w: f64, h: f64 },
}
impl FromStr for Shape {
type Err = String;
fn from_str(s: &str) -> Result<Shape, String> {
let parts: Vec<&str> = s.split_whitespace().collect();
let num = |i: usize| -> Result<f64, String> {
let raw = parts.get(i).ok_or_else(|| format!("{s:?}: missing value"))?;
raw.parse::<f64>().map_err(|e| format!("{s:?}: {e}"))
};
match parts.first().copied() {
Some("circle") => Ok(Shape::Circle { radius: num(1)? }),
Some("square") => Ok(Shape::Square { side: num(1)? }),
Some("rect") => Ok(Shape::Rect { w: num(1)?, h: num(2)? }),
Some(other) => Err(format!("unknown shape {other:?}")),
None => Err(String::from("empty input")),
}
}
}
fn main() {
for line in ["circle 1.5", "rect 2 3", "square x", "hexagon 4", "rect 2"] {
match line.parse::<Shape>() {
Ok(shape) => println!("{shape:?}"),
Err(e) => println!("error: {e}"),
}
}
}Circle { radius: 1.5 }
Rect { w: 2.0, h: 3.0 }
error: "square x": invalid float literal
error: unknown shape "hexagon"
error: "rect 2": missing value8. A registry of constructors
Plugin systems look up a factory by name. In Rust, a HashMap from names to constructor functions does the job. Closures that capture nothing convert to plain function pointers (fn(...) types), so the registry needs no boxing.
use std::collections::HashMap;
trait Compressor {
fn name(&self) -> String;
fn compress(&self, data: &str) -> String;
}
struct Identity;
struct RunLength;
struct Truncate {
max: usize,
}
impl Compressor for Identity {
fn name(&self) -> String {
String::from("none")
}
fn compress(&self, data: &str) -> String {
data.to_string()
}
}
impl Compressor for RunLength {
fn name(&self) -> String {
String::from("rle")
}
fn compress(&self, data: &str) -> String {
let mut out = String::new();
let mut chars = data.chars().peekable();
while let Some(c) = chars.next() {
let mut count = 1;
while chars.peek() == Some(&c) {
chars.next();
count += 1;
}
out.push_str(&format!("{c}{count}"));
}
out
}
}
impl Compressor for Truncate {
fn name(&self) -> String {
format!("truncate({})", self.max)
}
fn compress(&self, data: &str) -> String {
data.chars().take(self.max).collect()
}
}
type Constructor = fn(&str) -> Result<Box<dyn Compressor>, String>;
fn registry() -> HashMap<&'static str, Constructor> {
let mut r: HashMap<&'static str, Constructor> = HashMap::new();
r.insert("none", |_| Ok(Box::new(Identity)));
r.insert("rle", |_| Ok(Box::new(RunLength)));
r.insert("truncate", |arg| {
let max = arg.parse().map_err(|_| format!("truncate needs a number, got {arg:?}"))?;
Ok(Box::new(Truncate { max }))
});
r
}
fn create(registry: &HashMap<&'static str, Constructor>, spec: &str) -> Result<Box<dyn Compressor>, String> {
let (name, arg) = spec.split_once(':').unwrap_or((spec, ""));
let constructor = registry.get(name).ok_or_else(|| format!("no compressor named {name:?}"))?;
constructor(arg)
}
fn main() {
let registry = registry();
for spec in ["rle", "truncate:4", "none", "truncate:many", "zip"] {
match create(®istry, spec) {
Ok(c) => println!("{:<12} {}", c.name(), c.compress("aaaabbbcca")),
Err(e) => println!("error: {e}"),
}
}
}rle a4b3c2a1
truncate(4) aaaa
none aaaabbbcca
error: truncate needs a number, got "many"
error: no compressor named "zip"9. Factory closures
In Rust, a factory is a closure parameter. The standard library uses the same idea everywhere: resize_with, or_insert_with, and unwrap_or_else all take a closure that creates a value only when one is needed.
#[derive(Debug)]
struct Worker {
id: usize,
queue: String,
}
fn spawn_pool(size: usize, make: impl Fn(usize) -> Worker) -> Vec<Worker> {
(0..size).map(make).collect()
}
fn main() {
let fast = spawn_pool(2, |id| Worker { id, queue: String::from("fast") });
let prefix = "batch";
let batch = spawn_pool(3, |id| Worker { id: 100 + id, queue: format!("{prefix}-{}", id % 2) });
println!("{fast:?}");
println!("{batch:?}");
let mut grid: Vec<Vec<char>> = Vec::new();
grid.resize_with(2, || vec!['.'; 3]);
println!("{grid:?}");
}[Worker { id: 0, queue: "fast" }, Worker { id: 1, queue: "fast" }]
[Worker { id: 100, queue: "batch-0" }, Worker { id: 101, queue: "batch-1" }, Worker { id: 102, queue: "batch-0" }]
[['.', '.', '.'], ['.', '.', '.']]10. Abstract factory with associated types
An abstract factory creates a family of related objects. A trait with associated types ties the family together at compile time, so a light-theme button can never be paired with a dark-theme dialog.
trait Button {
fn render(&self, label: &str) -> String;
}
trait Dialog {
fn render(&self, text: &str) -> String;
}
trait ThemeFactory {
type B: Button;
type D: Dialog;
fn button(&self) -> Self::B;
fn dialog(&self) -> Self::D;
}
struct LightButton;
struct LightDialog;
struct DarkButton;
struct DarkDialog;
impl Button for LightButton {
fn render(&self, l: &str) -> String {
format!("[ {l} ]")
}
}
impl Dialog for LightDialog {
fn render(&self, t: &str) -> String {
format!("+-- {t} --+")
}
}
impl Button for DarkButton {
fn render(&self, l: &str) -> String {
format!("[# {l} #]")
}
}
impl Dialog for DarkDialog {
fn render(&self, t: &str) -> String {
format!("#== {t} ==#")
}
}
struct Light;
struct Dark;
impl ThemeFactory for Light {
type B = LightButton;
type D = LightDialog;
fn button(&self) -> LightButton {
LightButton
}
fn dialog(&self) -> LightDialog {
LightDialog
}
}
impl ThemeFactory for Dark {
type B = DarkButton;
type D = DarkDialog;
fn button(&self) -> DarkButton {
DarkButton
}
fn dialog(&self) -> DarkDialog {
DarkDialog
}
}
fn confirm_screen(theme: &impl ThemeFactory) -> String {
format!("{} {}", theme.dialog().render("Delete file?"), theme.button().render("OK"))
}
fn main() {
println!("{}", confirm_screen(&Light));
println!("{}", confirm_screen(&Dark));
}+-- Delete file? --+ [ OK ]
#== Delete file? ==# [# OK #]11. Abstract factory chosen at runtime
When the family is picked at runtime, for example from the platform or a setting, the factory methods return trait objects instead. You lose the compile-time pairing guarantee of example 10, and gain the ability to decide while the program runs.
trait Button {
fn render(&self, label: &str) -> String;
}
trait Checkbox {
fn render(&self, label: &str, checked: bool) -> String;
}
trait WidgetFactory {
fn button(&self) -> Box<dyn Button>;
fn checkbox(&self) -> Box<dyn Checkbox>;
}
struct AsciiButton;
struct AsciiCheckbox;
struct FancyButton;
struct FancyCheckbox;
impl Button for AsciiButton {
fn render(&self, label: &str) -> String {
format!("< {label} >")
}
}
impl Checkbox for AsciiCheckbox {
fn render(&self, label: &str, checked: bool) -> String {
format!("[{}] {label}", if checked { 'x' } else { ' ' })
}
}
impl Button for FancyButton {
fn render(&self, label: &str) -> String {
format!("【 {label} 】")
}
}
impl Checkbox for FancyCheckbox {
fn render(&self, label: &str, checked: bool) -> String {
format!("{} {label}", if checked { '☑' } else { '☐' })
}
}
struct AsciiWidgets;
struct FancyWidgets;
impl WidgetFactory for AsciiWidgets {
fn button(&self) -> Box<dyn Button> {
Box::new(AsciiButton)
}
fn checkbox(&self) -> Box<dyn Checkbox> {
Box::new(AsciiCheckbox)
}
}
impl WidgetFactory for FancyWidgets {
fn button(&self) -> Box<dyn Button> {
Box::new(FancyButton)
}
fn checkbox(&self) -> Box<dyn Checkbox> {
Box::new(FancyCheckbox)
}
}
fn widgets_for(terminal: &str) -> Box<dyn WidgetFactory> {
match terminal {
"dumb" | "vt100" => Box::new(AsciiWidgets),
_ => Box::new(FancyWidgets),
}
}
fn settings_screen(ui: &dyn WidgetFactory) -> String {
let check = ui.checkbox();
[check.render("dark mode", true), check.render("beta features", false), ui.button().render("Save")].join(" ")
}
fn main() {
for terminal in ["vt100", "xterm-256color"] {
println!("{terminal:>14}: {}", settings_screen(widgets_for(terminal).as_ref()));
}
} vt100: [x] dark mode [ ] beta features < Save >
xterm-256color: ☑ dark mode ☐ beta features 【 Save 】Part 4: Builder
12. A consuming builder
A builder collects options step by step and creates the value at the end. Each method takes self by value and returns it, so calls chain. Rust's naming convention is a ...Builder type whose build method produces the value.
#[derive(Debug)]
struct Pizza {
size: u8,
toppings: Vec<String>,
extra_cheese: bool,
}
struct PizzaBuilder {
size: u8,
toppings: Vec<String>,
extra_cheese: bool,
}
impl PizzaBuilder {
fn new(size: u8) -> Self {
PizzaBuilder { size, toppings: Vec::new(), extra_cheese: false }
}
fn topping(mut self, t: &str) -> Self {
self.toppings.push(t.to_string());
self
}
fn extra_cheese(mut self) -> Self {
self.extra_cheese = true;
self
}
fn build(self) -> Pizza {
Pizza { size: self.size, toppings: self.toppings, extra_cheese: self.extra_cheese }
}
}
fn main() {
let p = PizzaBuilder::new(32).topping("mushroom").topping("olive").extra_cheese().build();
println!("{p:?}");
}Pizza { size: 32, toppings: ["mushroom", "olive"], extra_cheese: true }13. A builder that validates
Two rules make builders safer. Required values go into new, so they can't be forgotten. Rules that involve several fields are checked once, in build, which returns a Result.
#[derive(Debug)]
struct Request {
method: String,
url: String,
headers: Vec<(String, String)>,
body: Option<String>,
timeout_secs: u64,
}
struct RequestBuilder {
method: String,
url: String,
headers: Vec<(String, String)>,
body: Option<String>,
timeout_secs: u64,
}
impl RequestBuilder {
fn new(method: &str, url: &str) -> RequestBuilder {
RequestBuilder { method: method.to_uppercase(), url: url.to_string(), headers: Vec::new(), body: None, timeout_secs: 30 }
}
fn header(mut self, name: &str, value: &str) -> Self {
self.headers.push((name.to_string(), value.to_string()));
self
}
fn body(mut self, body: &str) -> Self {
self.body = Some(body.to_string());
self
}
fn timeout_secs(mut self, secs: u64) -> Self {
self.timeout_secs = secs;
self
}
fn build(self) -> Result<Request, String> {
if !self.url.starts_with("http://") && !self.url.starts_with("https://") {
return Err(format!("unsupported URL {:?}", self.url));
}
if self.method == "GET" && self.body.is_some() {
return Err(String::from("a GET request can't have a body"));
}
if self.timeout_secs == 0 {
return Err(String::from("timeout must be at least 1 second"));
}
Ok(Request { method: self.method, url: self.url, headers: self.headers, body: self.body, timeout_secs: self.timeout_secs })
}
}
fn main() {
let ok = RequestBuilder::new("post", "https://api.example.com/orders")
.header("Content-Type", "application/json")
.body(r#"{"sku":"A-1","qty":2}"#)
.timeout_secs(5)
.build();
println!("{ok:#?}");
let with_body = RequestBuilder::new("get", "https://api.example.com/orders").body("oops").build();
println!("{with_body:?}");
println!("{:?}", RequestBuilder::new("get", "ftp://files.example.com").build().map(|r| r.url));
}Ok(
Request {
method: "POST",
url: "https://api.example.com/orders",
headers: [
(
"Content-Type",
"application/json",
),
],
body: Some(
"{\"sku\":\"A-1\",\"qty\":2}",
),
timeout_secs: 5,
},
)
Err("a GET request can't have a body")
Err("unsupported URL \"ftp://files.example.com\"")A builder can even make a forgotten step a compile error, by changing its type after each required call. That technique, typestate, is shown in API Design Idioms later in the course.
14. A &mut self builder for conditional steps
Consuming builders chain nicely, but conditional steps get clumsy (builder = builder.filter(...) inside every if). Builders whose methods take and return &mut Self can be modified in loops and ifs, then built. std::process::Command works this way.
#[derive(Default)]
struct QueryBuilder {
table: String,
filters: Vec<String>,
order_by: Option<String>,
limit: Option<u32>,
}
impl QueryBuilder {
fn select_from(table: &str) -> QueryBuilder {
QueryBuilder { table: table.to_string(), ..Default::default() }
}
fn filter(&mut self, condition: &str) -> &mut Self {
self.filters.push(condition.to_string());
self
}
fn order_by(&mut self, column: &str) -> &mut Self {
self.order_by = Some(column.to_string());
self
}
fn limit(&mut self, n: u32) -> &mut Self {
self.limit = Some(n);
self
}
fn build(&self) -> String {
let mut sql = format!("SELECT * FROM {}", self.table);
if !self.filters.is_empty() {
sql += &format!(" WHERE {}", self.filters.join(" AND "));
}
if let Some(column) = &self.order_by {
sql += &format!(" ORDER BY {column}");
}
if let Some(n) = self.limit {
sql += &format!(" LIMIT {n}");
}
sql
}
}
struct SearchForm {
name: Option<&'static str>,
min_age: Option<u32>,
tags: Vec<&'static str>,
page_size: u32,
}
fn main() {
let form = SearchForm { name: Some("ana"), min_age: None, tags: vec!["admin", "beta"], page_size: 20 };
let mut query = QueryBuilder::select_from("users");
if let Some(name) = form.name {
query.filter(&format!("name = '{name}'"));
}
if let Some(age) = form.min_age {
query.filter(&format!("age >= {age}"));
}
for tag in &form.tags {
query.filter(&format!("'{tag}' = ANY(tags)"));
}
query.order_by("created_at").limit(form.page_size);
println!("{}", query.build());
println!("{}", QueryBuilder::select_from("orders").limit(5).build());
}SELECT * FROM users WHERE name = 'ana' AND 'admin' = ANY(tags) AND 'beta' = ANY(tags) ORDER BY created_at LIMIT 20
SELECT * FROM orders LIMIT 5This builds SQL text only to show the builder's shape. Real code never pastes user input into SQL; it passes values as bound parameters, as the database lesson shows later.
Part 5: Prototype
15. Prototype: Clone
The prototype pattern creates objects by copying a configured template. In Rust that's #[derive(Clone)] plus struct update syntax, and clone() is a deep copy by default, field by field.
#[derive(Debug, Clone)]
struct Enemy {
kind: String,
health: u32,
speed: f32,
loot: Vec<String>,
}
fn main() {
let goblin_template = Enemy { kind: String::from("goblin"), health: 30, speed: 1.2, loot: vec![String::from("coin")] };
let mut boss = Enemy { health: 300, kind: String::from("goblin king"), ..goblin_template.clone() };
boss.loot.push(String::from("crown")); // the template's loot list is untouched
let wave: Vec<Enemy> = (0..3).map(|_| goblin_template.clone()).collect();
println!("{} goblins with {} hp; template loot {:?}", wave.len(), wave[0].health, goblin_template.loot);
println!("{boss:?}");
}3 goblins with 30 hp; template loot ["coin"]
Enemy { kind: "goblin king", health: 300, speed: 1.2, loot: ["coin", "crown"] }16. Prototypes behind a trait object
A registry of prototypes usually stores them as trait objects. Clone can't be used through dyn Trait (its clone returns Self, whose size isn't known), so the trait gets a clone_box method that returns a boxed copy, and Box<dyn Shape> implements Clone by calling it (Trait Objects introduced this trick).
use std::collections::HashMap;
trait Shape {
fn describe(&self) -> String;
fn move_to(&mut self, x: i32, y: i32);
fn clone_box(&self) -> Box<dyn Shape>;
}
#[derive(Clone)]
struct Label {
text: String,
font_size: u32,
pos: (i32, i32),
}
#[derive(Clone)]
struct Circle {
radius: u32,
fill: &'static str,
pos: (i32, i32),
}
impl Shape for Label {
fn describe(&self) -> String {
format!("label {:?} ({}pt) at {:?}", self.text, self.font_size, self.pos)
}
fn move_to(&mut self, x: i32, y: i32) {
self.pos = (x, y);
}
fn clone_box(&self) -> Box<dyn Shape> {
Box::new(self.clone())
}
}
impl Shape for Circle {
fn describe(&self) -> String {
format!("{} circle r={} at {:?}", self.fill, self.radius, self.pos)
}
fn move_to(&mut self, x: i32, y: i32) {
self.pos = (x, y);
}
fn clone_box(&self) -> Box<dyn Shape> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn Shape> {
fn clone(&self) -> Self {
self.clone_box()
}
}
fn main() {
let mut palette: HashMap<&str, Box<dyn Shape>> = HashMap::new();
palette.insert("title", Box::new(Label { text: String::from("Untitled"), font_size: 32, pos: (0, 0) }));
palette.insert("dot", Box::new(Circle { radius: 4, fill: "red", pos: (0, 0) }));
let mut canvas: Vec<Box<dyn Shape>> = Vec::new();
for (name, x, y) in [("title", 10, 5), ("dot", 3, 3), ("dot", 8, 3)] {
let mut shape = palette[name].clone(); // copy the prototype
shape.move_to(x, y);
canvas.push(shape);
}
for shape in &canvas {
println!("{}", shape.describe());
}
println!("prototype unchanged: {}", palette["dot"].describe());
}label "Untitled" (32pt) at (10, 5)
red circle r=4 at (3, 3)
red circle r=4 at (8, 3)
prototype unchanged: red circle r=4 at (0, 0)Part 6: Dependency injection
17. Constructor injection with generics
Constructor injection needs no framework: a struct receives its dependencies. Generics wire them at compile time, so each call is resolved statically.
trait Clock {
fn now(&self) -> u64;
}
trait Repo {
fn last_login(&self, user: &str) -> Option<u64>;
}
struct SystemClock;
impl Clock for SystemClock {
fn now(&self) -> u64 {
1_700_000_000
}
}
struct FixedRepo;
impl Repo for FixedRepo {
fn last_login(&self, user: &str) -> Option<u64> {
(user == "ana").then_some(1_699_990_000)
}
}
// Dependencies come in through the constructor.
struct InactivityChecker<C: Clock, R: Repo> {
clock: C,
repo: R,
max_idle_secs: u64,
}
impl<C: Clock, R: Repo> InactivityChecker<C, R> {
fn is_inactive(&self, user: &str) -> bool {
match self.repo.last_login(user) {
Some(t) => self.clock.now() - t > self.max_idle_secs,
None => true,
}
}
}
fn main() {
let checker = InactivityChecker { clock: SystemClock, repo: FixedRepo, max_idle_secs: 86_400 };
println!("ana inactive: {}", checker.is_inactive("ana"));
println!("ben inactive: {}", checker.is_inactive("ben"));
let strict = InactivityChecker { clock: SystemClock, repo: FixedRepo, max_idle_secs: 3_600 };
println!("ana inactive (1h limit): {}", strict.is_inactive("ana"));
}ana inactive: false
ben inactive: true
ana inactive (1h limit): trueIn tests, pass a fake clock and an in-memory repo, as in the Testing lesson.
18. Injection chosen at runtime, with a composition root
When the implementations depend on configuration, store the dependencies as trait objects. One function, the composition root, decides which implementations to use; everything else just receives them. It's ordinary code, and a missing dependency is a compile error instead of a startup failure.
trait OrderRepo {
fn save(&mut self, id: u32, total_cents: u32) -> Result<(), String>;
}
trait Mailer {
fn send(&self, to: &str, text: &str) -> String;
}
struct PostgresRepo {
url: String,
}
struct InMemoryRepo(Vec<(u32, u32)>);
struct SmtpMailer {
host: String,
}
struct FakeMailer;
impl OrderRepo for PostgresRepo {
fn save(&mut self, id: u32, total_cents: u32) -> Result<(), String> {
println!(" (INSERT order {id}, {total_cents} into {})", self.url);
Ok(())
}
}
impl OrderRepo for InMemoryRepo {
fn save(&mut self, id: u32, total_cents: u32) -> Result<(), String> {
if self.0.iter().any(|(existing, _)| *existing == id) {
return Err(format!("order {id} already exists"));
}
self.0.push((id, total_cents));
Ok(())
}
}
impl Mailer for SmtpMailer {
fn send(&self, to: &str, text: &str) -> String {
format!("mail via {} to {to}: {text}", self.host)
}
}
impl Mailer for FakeMailer {
fn send(&self, to: &str, text: &str) -> String {
format!("(not sent) {to}: {text}")
}
}
struct OrderService {
repo: Box<dyn OrderRepo>,
mailer: Box<dyn Mailer>,
}
impl OrderService {
fn new(repo: Box<dyn OrderRepo>, mailer: Box<dyn Mailer>) -> OrderService {
OrderService { repo, mailer }
}
fn place(&mut self, id: u32, total_cents: u32, email: &str) -> Result<String, String> {
self.repo.save(id, total_cents)?;
Ok(self.mailer.send(email, &format!("order {id} confirmed")))
}
}
// The composition root.
fn build_service(env: &str) -> OrderService {
match env {
"prod" => OrderService::new(
Box::new(PostgresRepo { url: String::from("postgres://db/shop") }),
Box::new(SmtpMailer { host: String::from("smtp.example.com") }),
),
_ => OrderService::new(Box::new(InMemoryRepo(Vec::new())), Box::new(FakeMailer)),
}
}
fn main() {
for env in ["prod", "test"] {
println!("{env}:");
let mut service = build_service(env);
for id in [1, 2, 1] {
match service.place(id, 4_990, "ana@example.com") {
Ok(msg) => println!(" {msg}"),
Err(e) => println!(" error: {e}"),
}
}
}
}prod:
(INSERT order 1, 4990 into postgres://db/shop)
mail via smtp.example.com to ana@example.com: order 1 confirmed
(INSERT order 2, 4990 into postgres://db/shop)
mail via smtp.example.com to ana@example.com: order 2 confirmed
(INSERT order 1, 4990 into postgres://db/shop)
mail via smtp.example.com to ana@example.com: order 1 confirmed
test:
(not sent) ana@example.com: order 1 confirmed
(not sent) ana@example.com: order 2 confirmed
error: order 1 already existsThe pretend PostgresRepo doesn't check for duplicates while the in-memory one does, so the two runs differ on the third order. Swapping implementations is exactly what lets tests run without a database.
19. Realistic example: starting an application
A small application start-up that combines the patterns from this lesson: Default for settings, a factory method that reads overrides from configuration text, a factory choosing the storage backend, and an application context struct that is created once and passed around instead of globals.
trait Storage {
fn describe(&self) -> String;
}
struct MemoryStorage {
capacity: usize,
}
struct DiskStorage {
path: String,
}
impl Storage for MemoryStorage {
fn describe(&self) -> String {
format!("in-memory storage ({} entries)", self.capacity)
}
}
impl Storage for DiskStorage {
fn describe(&self) -> String {
format!("disk storage at {}", self.path)
}
}
#[derive(Debug, Clone)]
struct AppConfig {
name: String,
storage: String,
cache_size: usize,
debug: bool,
}
impl Default for AppConfig {
fn default() -> Self {
AppConfig { name: String::from("shop"), storage: String::from("memory"), cache_size: 100, debug: false }
}
}
impl AppConfig {
// Defaults, overridden by `key = value` lines from a file or the environment.
fn from_text(text: &str) -> Result<AppConfig, String> {
let mut config = AppConfig::default();
for line in text.lines().map(str::trim).filter(|l| !l.is_empty() && !l.starts_with('#')) {
let (key, value) = line.split_once('=').ok_or_else(|| format!("bad line {line:?}"))?;
let value = value.trim();
match key.trim() {
"name" => config.name = value.to_string(),
"storage" => config.storage = value.to_string(),
"cache_size" => config.cache_size = value.parse().map_err(|_| format!("cache_size: {value:?} is not a number"))?,
"debug" => config.debug = value == "true",
other => return Err(format!("unknown setting {other:?}")),
}
}
Ok(config)
}
}
fn storage_from(config: &AppConfig) -> Result<Box<dyn Storage>, String> {
match config.storage.as_str() {
"memory" => Ok(Box::new(MemoryStorage { capacity: config.cache_size })),
s if s.starts_with("disk:") => Ok(Box::new(DiskStorage { path: s["disk:".len()..].to_string() })),
other => Err(format!("unknown storage {other:?}")),
}
}
// The application context: built once, then passed by reference.
struct App {
config: AppConfig,
storage: Box<dyn Storage>,
}
impl App {
fn start(config_text: &str) -> Result<App, String> {
let config = AppConfig::from_text(config_text)?;
let storage = storage_from(&config)?;
Ok(App { config, storage })
}
}
fn banner(app: &App) -> String {
let debug = if app.config.debug { " [debug]" } else { "" };
format!("{} started with {}{debug}", app.config.name, app.storage.describe())
}
fn main() {
let configs = [
"",
"name = inventory\nstorage = disk:/var/lib/inventory\ndebug = true",
"# staging\ncache_size = lots",
"storage = s3",
];
for text in configs {
match App::start(text) {
Ok(app) => println!("{}", banner(&app)),
Err(e) => println!("startup failed: {e}"),
}
}
}shop started with in-memory storage (100 entries)
inventory started with disk storage at /var/lib/inventory [debug]
startup failed: cache_size: "lots" is not a number
startup failed: unknown storage "s3"Summary
| Pattern | Rust |
|---|---|
| static factory methods | named associated functions, returning Result when they can fail |
| telescoping constructors | Default + ..Default::default() |
| Singleton | LazyLock for computed globals, OnceLock for globals set at startup; prefer parameters |
| Factory method | a function returning Box<dyn Trait>, or an enum (via FromStr for text) |
| plugin lookup | a HashMap of fn constructors |
| factory as a value | a closure parameter, impl Fn() -> T |
| Abstract factory | a trait with associated types (compile time) or methods returning Box<dyn Trait> (runtime) |
| Builder | new takes required values; self methods chain, &mut self methods suit loops and ifs; build returns Result |
| Prototype | #[derive(Clone)]; clone_box for trait objects |
| Dependency injection | constructor parameters: generics or trait objects, wired in one composition root |
Next: Structural Patterns: adapters, decorators, composites, facades, proxies, and more, built from wrapper types and traits.
