Skip to content

Replace Type Code with State/Strategy

Take a type code that both drives varying behaviour and changes over the object’s lifetime — an order’s status, a connection’s mode — and replace it with a State (or Strategy) object the host holds and can swap. The host delegates the varying behaviour to that object; transitioning is just assigning a new state. Because the host’s class never changes, this works precisely where Replace Type Code with Subclasses cannot.

The smell is the same scattered switch over a type code as in the subclass refactoring, but with one extra property: the code changes at runtime. An order moves pending → shipped → delivered; a document goes draft → published → archived. You cannot model a changing identity by changing an object’s class — most languages will not let an object become a different class mid-life. So the branching keeps multiplying across methods, and every transition is a raw field reassignment with no guarantee the new value is even valid.

An Order whose status string is checked by a switch in two methods. After, the order holds an OrderState it delegates to and swaps on transition.

// Before
class Order {
constructor(public status: string) {}
label(): string {
switch (this.status) {
case 'pending': return 'Awaiting payment';
case 'shipped': return 'On the way';
case 'delivered': return 'Complete';
default: throw new Error(`unknown ${this.status}`);
}
}
canCancel(): boolean {
return this.status === 'pending';
}
}
// After
interface OrderState {
label(): string;
canCancel(): boolean;
}
const Pending: OrderState = { label: () => 'Awaiting payment', canCancel: () => true };
const Shipped: OrderState = { label: () => 'On the way', canCancel: () => false };
const Delivered: OrderState = { label: () => 'Complete', canCancel: () => false };
class Order {
constructor(private state: OrderState = Pending) {}
label(): string { return this.state.label(); }
canCancel(): boolean { return this.state.canCancel(); }
ship(): void { this.state = Shipped; } // a transition is a swap
}
flowchart TD
  subgraph Before["Before — a type code with switching"]
    A["Order<br/>status: 'pending' | 'shipped' | 'delivered'"]
    A --> B{"switch status"}
    B --> C["pending behaviour"]
    B --> D["shipped behaviour"]
    B --> E["delivered behaviour"]
  end
  subgraph After["After — a swappable state object"]
    F["Order<br/>holds a State"]
    F --> G["OrderState (interface)<br/>label() / canCancel()"]
    G --> H["Pending"]
    G --> I["Shipped"]
    G --> J["Delivered"]
  end
  Before -.->|"Replace Type Code with State/Strategy"| After
A runtime-changing type code becomes a swappable State object the host delegates to
  1. Encapsulate the type-code field behind accessors, so callers depend on a getter/setter rather than the raw value.
  2. Introduce a state interface declaring the operations that vary with the code.
  3. Create one state class per type-code value, moving each branch of the varying methods into its corresponding state.
  4. Give the host a field holding the current state, and have its methods delegate to that state. Run your tests after wiring each method.
  5. Replace each assignment of the type code with a swap of the state object — ideally through methods like ship() that name the transition and can guard which moves are legal.
  6. Once nothing reads the raw code, remove it. The host now varies behaviour purely by which state it holds.

Choose State/Strategy when the variant can change after construction. Because the host keeps its class and merely swaps a held object, it models a lifecycle that subclasses cannot. State suits a small set of named conditions with transitions between them (order status); Strategy suits injecting one interchangeable algorithm (a pricing policy). Both also let you guard transitions and even add per-state data the host need not carry.

Contrast this with Replace Type Code with Subclasses: that move bakes the variant into the object’s type, which is perfect when the type is fixed for life (an employee’s role) but impossible when it must change. The cost of State/Strategy is one more object and a layer of delegation; you pay it to gain a runtime-swappable, guardable lifecycle. If the type never changes, subclasses are simpler.

What extra property pushes you toward State/Strategy instead of subclasses?
After this refactoring, how is a transition between variants expressed?
When is Replace Type Code with Subclasses the better choice?
What is the typical cost of choosing State/Strategy?