2.0 Creational Patterns
Creational patterns address object construction. They decouple the code that
uses an object from the code that creates it, keeping clients ignorant of
which concrete type they hold and how it was built.
This separation matters when the concrete type varies by configuration, when
construction is complex, or when only one instance should exist. The three
patterns here cover those three cases: Factory Method for type selection,
Builder for step-by-step construction, and Singleton for single-instance control.
2.1 Factory Method
Intent: define an interface for creating an object, but let the
factory function or subclass decide which concrete type to instantiate. Clients
work with the product interface, never with constructors directly.
The factory function examines a key - a string, enum, or configuration value - and
returns the appropriate concrete implementation. The caller receives a
Box<dyn Product> or interface reference and never imports the
concrete type.
classDiagram
class make_connection {
<<factory>>
+make_connection(kind, addr) Box~dyn Connection~
}
class Connection {
<<interface>>
+send(data)
+recv() Vec~u8~
+close()
}
class TcpConnection {
+send(data)
+recv() Vec~u8~
+close()
}
class UnixConnection {
+send(data)
+recv() Vec~u8~
+close()
}
class MockConnection {
+send(data)
+recv() Vec~u8~
+close()
}
make_connection ..> Connection : returns
Connection <|.. TcpConnection : implements
Connection <|.. UnixConnection : implements
Connection <|.. MockConnection : implements
interface Connection {
fn send(&self, data: &[u8]) -> Result
fn recv(&mut self) -> Result<Vec<u8>>
fn close(&mut self)
}
struct TcpConnection { ... }
struct UnixConnection { ... }
struct MockConnection { ... }
impl Connection for TcpConnection { ... }
impl Connection for UnixConnection { ... }
impl Connection for MockConnection { ... }
fn make_connection(kind: &str, addr: &str) -> Box<dyn Connection> {
match kind {
"tcp" => Box::new(TcpConnection::new(addr)),
"unix" => Box::new(UnixConnection::new(addr)),
"mock" => Box::new(MockConnection::new()),
_ => panic!("unknown connection kind"),
}
}
The caller passes "tcp" in production and "mock" in tests.
Neither the caller's code nor its tests import the concrete connection types.
Factory Method - Language Idioms
| Language | Idiomatic form |
| Rust |
A free function or associated function returning Box<dyn Trait>.
An enum works when the set of concrete types is closed and all variants are
compiled in.
|
| C++ |
Static factory function returning std::unique_ptr<Base>.
Template factories are possible when the type is a compile-time parameter.
|
| C# |
Static method on the interface or a registry class; generics with
new() constraint for compile-time factories.
Activator.CreateInstance() for reflection-based factories.
|
| Python |
A plain function or a registry dict mapping keys to classes;
call the class as a constructor. Module-level factory functions are idiomatic.
|
Factory Method - Pros and Cons
| Notes |
| Pros |
Clients are decoupled from concrete types; swapping implementations
(production vs. test, platform-specific vs. generic) requires no
changes to calling code; the factory is the single point to update
when adding a new variant.
|
| Cons |
The factory must know all concrete types, creating a coupling point;
dynamically loaded types require a registration mechanism; error handling
for unknown keys must be decided at the factory boundary.
|
| Best for |
Anything where the concrete type varies by configuration, platform, or
environment - connection types, serialization formats, UI controls, logging
backends.
|
2.2 Builder
Intent: separate the step-by-step construction of a complex object
from its representation. The same build process can produce different representations
by supplying different parameters.
The Builder accumulates parts through a chain of method calls, each setting one
configuration value. A final build() call validates and returns the
finished product. This eliminates constructors with long parameter lists and
makes optional fields explicit.
classDiagram
class SearchConfigBuilder {
-config : SearchConfig
+new(root, regex) SearchConfigBuilder
+extensions(exts) SearchConfigBuilder
+recursive(r) SearchConfigBuilder
+max_depth(d) SearchConfigBuilder
+build() Result~SearchConfig~
}
class SearchConfig {
+root : PathBuf
+extensions : Vec~String~
+regex : String
+recursive : bool
+max_depth : Option~usize~
}
SearchConfigBuilder ..> SearchConfig : builds
struct SearchConfig {
root: PathBuf,
extensions: Vec<String>,
regex: String,
recursive: bool,
max_depth: Option<usize>,
}
struct SearchConfigBuilder { config: SearchConfig }
impl SearchConfigBuilder {
fn new(root: &str, regex: &str) -> Self { ... } // required fields only
fn extensions(mut self, exts: &[&str]) -> Self { ... }
fn recursive(mut self, r: bool) -> Self { ... }
fn max_depth(mut self, d: usize) -> Self { ... }
fn build(self) -> Result<SearchConfig, String> { ... }
}
// usage:
let cfg = SearchConfigBuilder::new("./src", r"\bTODO\b")
.extensions(&["rs", "toml"])
.recursive(true)
.max_depth(5)
.build()?;
Any combination of optional settings is valid. The caller only sets what it cares
about. build() validates that required fields are consistent and
returns an error if not.
Builder - Language Idioms
| Language | Idiomatic form |
| Rust |
Method chaining with consuming self prevents using the builder
after build(). The derive_builder crate generates
builder structs automatically from annotated structs.
|
| C++ |
Named parameter idiom: each setter returns Builder&
for chaining. The builder is separate from the product. Alternatively,
a config struct with designated initializers (C++20) covers simple cases.
|
| C# |
Object initializers (new Config { Root = "...", Recursive = true })
handle simple cases. Fluent builder classes with method chaining are used
when validation at Build() time is needed.
|
| Python |
Keyword arguments with defaults cover most Builder use cases directly.
A dataclass with @dataclass(frozen=True) and a factory
function handles validation cleanly.
|
Builder - Pros and Cons
| Notes |
| Pros |
Eliminates telescoping constructors; optional fields are explicit and named;
validation is centralized in build(); the same builder can produce
variants (test config vs. production config).
|
| Cons |
More code than a plain constructor for simple objects; the builder and product
types must stay in sync when fields are added; method chaining is unfamiliar
to some readers.
|
| Best for |
Objects with many optional parameters, complex invariants to validate at
construction, or configuration types assembled from external input (CLI args,
config files, network responses).
|
2.3 Singleton
Intent: ensure a class or struct has exactly one instance and
provide a global access point to it.
The classic form hides the constructor and provides a static instance()
method that creates the instance on first call and returns the same instance
on every subsequent call. Thread-safe initialization requires special care.
classDiagram
class AppConfig {
+log_level : String
+max_workers : usize
}
class CONFIG {
<<OnceLock~AppConfig~>>
+get_or_init(f) AppConfig
}
class config {
<<fn>>
+config() ~static AppConfig
}
config ..> CONFIG : reads
CONFIG o--> AppConfig : holds one instance
// Rust - thread-safe singleton with OnceLock (stable since 1.70)
use std::sync::OnceLock;
struct AppConfig { log_level: String, max_workers: usize }
static CONFIG: OnceLock<AppConfig> = OnceLock::new();
fn config() -> &'static AppConfig {
CONFIG.get_or_init(|| AppConfig {
log_level: std::env::var("LOG_LEVEL").unwrap_or("info".into()),
max_workers: num_cpus::get(),
})
}
The pattern solves a real problem - shared immutable configuration accessed from
many call sites - but introduces global state. Global state couples all callers
to a single instance, making components harder to test in isolation.
Singleton - When to Use and When to Avoid
| Notes |
| Appropriate uses |
Read-only configuration loaded once at startup; a logger writing to a fixed
destination; a connection pool shared across a process; a registry that
accumulates entries during initialization.
|
| Problematic uses |
Mutable shared state - causes races and makes reasoning difficult; anything
that varies between tests - the singleton persists across test runs in the
same process; anything that should vary by environment (production vs. test).
|
| Preferred alternative |
Inject the shared instance as a parameter. The caller decides whether to
share one instance or create separate ones. Tests inject a mock or a
freshly constructed value. This is the Dependency Injection pattern applied
to the same problem Singleton solves.
|
Singleton - Language Idioms
| Language | Idiomatic form |
| Rust |
std::sync::OnceLock<T> (1.70+) or the
once_cell crate for earlier versions. Both are thread-safe
without explicit locking.
|
| C++ |
Meyers singleton: a static local variable inside instance()
is initialized on first call and is thread-safe since C++11. Avoid
static class members with double-checked locking.
|
| C# |
Lazy<T> with thread-safe mode; the CLR guarantees
type initializers run exactly once, so a static readonly
field initialized inline is also safe.
|
| Python |
Module-level instances: Python imports a module once per process, so a
module-level object is effectively a singleton. Metaclass-based singletons
exist but are rarely necessary.
|
2.4 References
Creational Patterns References