Replace Constructor with Factory Function
Intent
Section titled “Intent”Replace a direct call to a constructor with a call to a factory function. A constructor is constrained: it must share the type’s name and it always returns an instance of exactly that type. A factory function is free — it can have a descriptive name, choose which subtype to return, hand back a cached instance, or validate before it builds. Callers ask the factory for what they want, and the factory decides how to make it.
The smell
Section titled “The smell”A raw constructor leaks construction decisions onto every caller. You see new Employee("Sara", "manager") and the caller is left to remember which string means what. Worse, when the type has variants — a manager is really a different kind of employee than an engineer — the bare constructor cannot return the right subtype, so callers branch on a type code themselves. The constructor’s name is fixed to the class, so it cannot say hire or fromJson or default. Each of these is the constructor being too limiting for the job.
Before → After
Section titled “Before → After”An employee type whose constructor takes a type string. After, a factory function picks the right subtype and reads clearly.
// Beforeclass Employee { constructor(public name: string, public type: string) {} monthlyBonus(): number { return this.type === "manager" ? 1000 : 200; }}
const e = new Employee("Sara", "manager");
// Afterabstract class Employee { constructor(public name: string) {} abstract monthlyBonus(): number;}
class Manager extends Employee { monthlyBonus(): number { return 1000; }}
class Engineer extends Employee { monthlyBonus(): number { return 200; }}
function hire(name: string, role: string): Employee { switch (role) { case "manager": return new Manager(name); case "engineer": return new Engineer(name); default: throw new Error(`Unknown role: ${role}`); }}
const e = hire("Sara", "manager");# Beforeclass Employee: def __init__(self, name, type): self.name = name self.type = type
def monthly_bonus(self): return 1000 if self.type == "manager" else 200
e = Employee("Sara", "manager")
# Afterfrom abc import ABC, abstractmethod
class Employee(ABC): def __init__(self, name): self.name = name
@abstractmethod def monthly_bonus(self): ...
class Manager(Employee): def monthly_bonus(self): return 1000
class Engineer(Employee): def monthly_bonus(self): return 200
def hire(name, role): builders = {"manager": Manager, "engineer": Engineer} if role not in builders: raise ValueError(f"Unknown role: {role}") return builders[role](name)
e = hire("Sara", "manager")// Beforetype Employee struct { Name string Type string}
func (e Employee) MonthlyBonus() float64 { if e.Type == "manager" { return 1000 } return 200}
e := Employee{Name: "Sara", Type: "manager"}
// Aftertype Employee interface { MonthlyBonus() float64}
type Manager struct{ Name string }
func (m Manager) MonthlyBonus() float64 { return 1000 }
type Engineer struct{ Name string }
func (e Engineer) MonthlyBonus() float64 { return 200 }
func Hire(name, role string) (Employee, error) { switch role { case "manager": return Manager{Name: name}, nil case "engineer": return Engineer{Name: name}, nil default: return nil, fmt.Errorf("unknown role: %s", role) }}
e, err := Hire("Sara", "manager")// Beforestruct Employee { name: String, kind: String,}
impl Employee { fn monthly_bonus(&self) -> u32 { if self.kind == "manager" { 1000 } else { 200 } }}
let e = Employee { name: "Sara".to_string(), kind: "manager".to_string() };
// Aftertrait Employee { fn monthly_bonus(&self) -> u32;}
struct Manager { name: String,}
impl Employee for Manager { fn monthly_bonus(&self) -> u32 { 1000 }}
struct Engineer { name: String,}
impl Employee for Engineer { fn monthly_bonus(&self) -> u32 { 200 }}
fn hire(name: &str, role: &str) -> Result<Box<dyn Employee>, String> { match role { "manager" => Ok(Box::new(Manager { name: name.to_string() })), "engineer" => Ok(Box::new(Engineer { name: name.to_string() })), other => Err(format!("unknown role: {other}")), }}
let e = hire("Sara", "manager")?;Mechanics
Section titled “Mechanics”- Write a factory function whose body does nothing but call the existing constructor and return the result. Give it a name that says what it makes, like
hireordefaultConfig. - Redirect callers to the factory one at a time, replacing each constructor call with a factory call. Run your tests after each.
- Once every caller goes through the factory, you have a single chokepoint. Now you can enrich it: validate arguments, return a cached instance, or branch to a subtype.
- If you are introducing subtypes, create them, move each branch’s behaviour onto the matching subtype, and let the factory return the right one based on its input.
- Run your tests after each change. The factory’s signature stays stable for callers even as what it builds evolves behind it.
- If the language allows, make the raw constructor private or non-public so the factory is the only supported way in.
When to use / trade-offs
Section titled “When to use / trade-offs”Reach for a factory when construction needs a clearer name than the class allows, when the right concrete type depends on the inputs, when you want to hide or pool instances, or when you want to validate before an object exists. The factory becomes the single, well-named door through which objects enter the system.
The trade-off is a layer of indirection: callers no longer see new and the concrete type directly, which can make navigation slightly less obvious. For a plain value type with one shape and no variants, a direct constructor is simpler and a factory is needless ceremony. Add the factory when the constructor’s rigidity starts to cost you — not before.