Skip to content
aipatterns.dev

Refactoring techniqueSimplifying Method Calls

Replace Constructor with Factory Method

Wrap object creation in a named static or module function when a plain constructor cannot express intent, pick a subtype, or reuse instances.

Also known as
Replace Constructor with Factory Function

Problem

A constructor has to do more than a constructor can express well. Callers pass a flag to get different variants, several constructors differ only by argument order, or creation should sometimes return a cached instance or a subclass. new always returns a fresh object of exactly one class, with no name to explain which variant you get.

Solution

Create a static method (or module-level function) that builds the object, give it a name that describes the variant, make the constructor private, and route callers through the factory.

Before

TypeScript
class Discount {
  constructor(
    readonly kind: "percent" | "fixed",
    readonly value: number,
    readonly currency?: string,
  ) {
    if (kind === "percent" && (value <= 0 || value > 100)) throw new RangeError("bad percent");
    if (kind === "fixed" && !currency) throw new Error("fixed discount needs a currency");
  }
}

const promo = new Discount("percent", 15);
const voucher = new Discount("fixed", 2000, "EUR");

After

TypeScript
class Discount {
  private constructor(
    readonly kind: "percent" | "fixed",
    readonly value: number,
    readonly currency?: string,
  ) {}

  static percent(rate: number): Discount {
    if (rate <= 0 || rate > 100) throw new RangeError("bad percent");
    return new Discount("percent", rate);
  }

  static fixed(amountCents: number, currency: string): Discount {
    return new Discount("fixed", amountCents, currency);
  }
}

const promo = Discount.percent(15);
const voucher = Discount.fixed(2000, "EUR");

Why

  • Each way of creating the object has a name and only the arguments it needs.
  • Invalid combinations (a percent with a currency, a fixed amount without one) cannot be written.
  • The factory can later return a subclass, a cached instance or a result type, without changing callers.

Steps

  1. Create a static factory that calls the existing constructor with the same arguments. Run the tests.
  2. Replace new calls with the factory, one module at a time. Run the tests.
  3. Make the constructor private (or unexported). The compiler now finds any missed new calls.
  4. Split the factory by variant, moving variant-specific validation into each one. Run the tests.

Watch out

  • Do not add a factory when a plain constructor with clear parameters works. new Point(x, y) needs no Point.of.
  • Private constructors block subclassing in some languages and break frameworks that instantiate classes reflectively (ORMs, DI containers, serializers). Check before you hide the constructor.
  • A factory that grows a switch over a type string is just moving the problem. Use Replace Type Code with Subclasses and let each subclass have its own factory, or a map.
  • The inverse, replacing a factory with a constructor, has no named technique. Do it when the factory adds nothing beyond new.

Agent checklist

  • The constructor needed a flag, had ambiguous overloads, or must return something other than a fresh instance.
  • Each factory has a name that describes the variant it creates.
  • No framework requires the constructor to stay public.
  • All new calls outside the class were migrated before the constructor went private.

Treats these smells

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.