skip to content

Which code smells motivate the refactorings "Replace Conditional Logic with Strategy" and "Replace State-Altering Conditionals with State", and how do you decide which of the two patterns the code actually wants?

level: middleimportance: must knowfreq 62%

answer

  1. growing/duplicated switch on a type code
  2. extract branch → extract class → delegate → delete conditional
  3. Strategy: chosen from outside, one algorithm
  4. State: chosen by own mode, branch sets next mode
  5. two stable branches ≠ refactor

basics

~20 s

Both start from big if/switch chains. Use Strategy when the branches are interchangeable ways to compute the same thing chosen by the caller or config. Use State when the branches depend on an object's current mode and the branches also decide the next mode.

solid answer

~50 s

The trigger smells are a conditional (if/else-if or switch) that keeps growing, the same conditional duplicated in several methods, and a type code or mode flag that other code branches on. The refactoring is the same in mechanics: extract each branch body into its own method, extract those into classes behind a common interface, delegate from the original method, then delete the conditional. What differs is the meaning of the branch key. In Strategy, branches are alternative algorithms for the same operation, selected from outside (configuration, request parameter, injected policy); the object doesn't switch strategies on its own. In State, the branch key is the object's own lifecycle mode (draft/submitted/approved), the branches depend on and *change* that mode, and behavior differs across several operations, not just one. Practical test: if the branch also assigns the field it switched on, that's State; if it is a pure pick-one-implementation, that's Strategy.

code

pseudocode · 18 lines
pseudocode
// BEFORE: growing conditional, duplicated across methods
function fee(order):
  if order.kind == "STANDARD": return 5
  if order.kind == "EXPRESS":  return 15
  if order.kind == "FREIGHT":  return weight(order) * 0.8

// AFTER (Strategy): branch key selects an interchangeable algorithm
interface ShippingPolicy { fee(order) }
class StandardPolicy { fee(order) = 5 }
class ExpressPolicy  { fee(order) = 15 }
class FreightPolicy  { fee(order) = weight(order) * 0.8 }
policies = { STANDARD: StandardPolicy, EXPRESS: ExpressPolicy, FREIGHT: FreightPolicy }
function fee(order) = policies[order.kind].fee(order)

// AFTER (State): branch key is the object's mode AND the branch advances it
interface OrderState { submit(order); cancel(order) }
class Draft     { submit(o) { validate(o); o.state = Submitted } ; cancel(o) { o.state = Cancelled } }
class Submitted { submit(o) { error("already submitted") } ; cancel(o) { refund(o); o.state = Cancelled } }

go deeper

for a junior

Name the smell (big or repeated switch) and say each branch becomes a class behind a shared interface; State is for lifecycle modes, Strategy for interchangeable algorithms.

for a middle

Walk the mechanical steps (extract branch bodies, extract classes, delegate, replace conditional with lookup, delete) and give the decision test: does the branch set the field it switched on?

for a senior

Add the trade-offs — when NOT to refactor, where the selection ends up living, testability gains, Open/Closed framing, and the class-explosion risk in State.

for a principal

Discuss when a table-driven or workflow-engine state machine beats one-class-per-state, how persisted state maps to behavior objects, and how to keep the pattern from being applied prophylactically across the codebase.

## The smells 1. **Long/growing conditional** — an `if/else if/...` or `switch` where each new requirement adds another arm. Every arm change edits the same method, so unrelated reasons to change collide (an Open/Closed and Single Responsibility violation). 2. **Duplicated conditional** — the *same* switch on the same key appears in `price()`, `label()`, and `validate()`. Adding a new case means finding all copies; missing one is a silent bug. 3. **Type code / mode flag** — a field like `kind`, `type`, `status` whose value is tested everywhere. 4. **Conditional complexity in one class** — the class does the picking *and* every branch's work, so it has many reasons to change. ## The two destinations **Strategy**: an interface with one operation, several implementations, and a context object that holds one implementation and delegates to it. The choice comes from *outside* — a config value, a request field, dependency injection, a factory. **State**: an interface (often with several operations), one implementation per lifecycle state, and a context that delegates to its *current* state object; a state object can hand the context a different state, performing the transition. The choice comes from *inside* — the object's own history. ## Shared mechanics (the small steps) 1. Ensure tests cover every branch (branch coverage, not just line coverage). 2. **Extract each branch body** into its own well-named method on the current class. 3. **Introduce the abstraction**: create the interface with the operation signature the branches share; normalize the extracted methods to that signature (parameterize the differences). 4. **Extract Class** per branch, moving its extracted method in; keep the original method delegating. 5. **Replace the conditional with a lookup/creation step** — a map from key to instance, a factory method, or (for State) the current-state field. 6. **Delete the conditional**, inline anything left over, and run tests after every step. Each numbered step is independently green — that is what makes the refactoring safe on production code. ## Choosing between them | Question | Strategy | State | |---|---|---| | Who picks the branch? | outside caller/config | the object's own current mode | | Does the branch change the key it switched on? | no | yes — it triggers the transition | | How many operations vary together? | usually one | usually several, per mode | | Lifetime | often fixed for the object's life | changes over time | | Illegal-combination concern | none | yes — legal transitions matter | **Diagnostic that decides most real cases:** look inside the branch. If the branch body ends with something like `this.status = APPROVED`, the conditional is encoding a *state machine* and State is the honest model — the transition table then lives in one place per state instead of being smeared across methods. If the branch body just computes and returns, and nothing mutates the selector, it is Strategy. ## Trade-offs and edge cases - **Don't refactor a two-arm, stable conditional.** Two branches that haven't changed in two years are cheaper as an `if` than as three new types. The smell must be *growth or duplication*, not mere existence. - **A polymorphic subclass hierarchy may be better than either** when the varying behavior is intrinsic to the data and the objects are created once with the right type — Replace Conditional with Polymorphism is the simpler destination. - **State explodes if you model every flag as a state.** Combinations multiply (3 flags = 8 states). Prefer modeling one lifecycle axis; keep orthogonal flags as data. - **Strategy with a single implementation is over-application** — that's a candidate for refactoring *away* from the pattern. - **Where the selection lives still needs a home**: you moved the switch, you didn't delete the knowledge. It usually becomes a registry map, a factory, or DI wiring. Be honest that one small conditional (or a map lookup) remains at the edge. - **Serialization/persistence**: a State object graph often still persists as an enum/string column; the mapping layer converts. Don't try to persist behavior. ## Payoff After the refactoring, adding a new case is *adding a class and one registry line* instead of editing N methods — the Open/Closed benefit — and each case is unit-testable in isolation. The cost is more types and one indirection hop, which is exactly why you wait for the smell.

  • After the refactoring, where does the if/switch actually go — hasn't it just moved?
    Largely yes, and that's the honest answer: the *selection* survives as a map lookup, a factory method, or DI configuration at one edge of the system. The win is that the selection now exists in exactly one place instead of being duplicated in every method, and each branch's behavior is an isolated, independently testable class. If the selection is data-driven (a key from a request or DB column) it degenerates to a single map lookup.
  • When would you prefer Replace Conditional with Polymorphism over Strategy?
    When the varying behavior is intrinsic to the object's own type rather than a pluggable policy, and the object is created once with that type. Then you push each branch into a subclass of the existing hierarchy — no separate policy object, no delegation hop. Strategy is preferable when the behavior must vary independently of the object's class, change at runtime, be shared across unrelated types, or be composed/injected for testing.
  • How do you keep a State refactoring from producing an unmaintainable class explosion?
    Model one lifecycle axis, not every boolean. Orthogonal flags stay as data on the context. Keep transitions declared in the state classes (or one transition table) so the machine is readable in one place, and consider a table-driven state machine instead of one class per state when states are numerous but behavior per state is thin.

Strategy is choosing which route app you use to get to work — you pick it, and it doesn't change itself. State is a traffic light: the light's current color determines what it does next and it advances itself to the next color.

saying these in an interview costs you the question

  • "Any switch statement is a smell and must become polymorphism" — small, stable, non-duplicated conditionals are fine and cheaper.
  • Confusing State and Strategy because their class diagrams look identical; the difference is who selects the branch and whether transitions occur.
  • "The pattern removes the conditional entirely" — the selection remains as a lookup/factory/DI edge; it is centralized, not deleted.
  • Introducing a Strategy interface with exactly one implementation "for testability" when a simpler seam exists.
  • Doing the whole refactoring in one commit with no tests over the branches, then claiming behavior was preserved.
  • Turning every boolean flag into a State class, causing combinatorial class explosion.

context