SWDev Patterns

SWDev Patterns: Creational

factory method, builder, singleton

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
LanguageIdiomatic 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
LanguageIdiomatic 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
LanguageIdiomatic 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
ResourceDescription
Refactoring Guru - Factory Method Factory Method with class diagrams and cross-language examples.
Refactoring Guru - Builder Builder with step-by-step construction examples and director pattern.
Refactoring Guru - Singleton Singleton pattern, thread-safety notes, and critique.
Rust std - OnceLock Rust standard library documentation for the thread-safe once-initialization cell.