A reviewer blocks a change because adding a third payment method means adding a branch to a switch statement, which "violates open-closed". The team has two payment methods today and expects a fourth within a year. Which response is correct?
Show the full answer Hide the answer
What is being tested
Whether you can tell a principle from a rule. Open-closed says a module should be extensible without modifying it; it does not say that every conditional is a defect. The useful question is not "does this switch violate a principle" but "which axis of change is this design open along, and is that the axis that is actually going to move".
The mechanism
An abstraction buys extension along one axis and makes every other axis harder. A payment strategy interface makes adding a method cheap. It makes changing the shape of what a payment method does expensive, because that change now touches the interface, every implementation and every test double. With two implementations you do not yet know the shape, so the interface you extract is a guess, and a wrong guess is more expensive than the switch because it is harder to delete.
The practical rule is the rule of three, as Fowler states it in Refactoring (1999): one case is a function, two cases is a conditional, three cases tells you what varies. At three, the extraction is informed by evidence rather than by prediction. Prefer the conditional until then, and keep it small enough to read in one screen — a 12-line switch is a design, a 200-line switch is a module waiting to be extracted.
The second point the reviewer is missing: a switch in one place is better than conditionals scattered across the codebase. If the switch is the only place that knows about payment methods, the design is already localised and adding a branch is a one-file change a reviewer can verify. The failure the principle guards against is the shotgun change, where a new case means edits in nine files. Count the files, not the branches: 1 file touched is a localised design, 9 files touched is the failure the principle names.
Why the other options fail
- A plugin registry now is the sophistication-bias answer. It adds indirection, dynamic dispatch, registration order, and a failure mode where a method is registered twice or not at all, in exchange for a benefit that arrives when the fourth method lands. The honest comparison is a one-line switch addition against a day of framework plus permanent reading cost.
- A class per branch immediately satisfies the letter of the principle and usually makes the code worse at this size: two classes, an interface, a factory and four files to read where there was one function. It is the right end state and the wrong next step.
- Configuration-driven dispatch sounds like it removes the code change, and mostly it relocates it. Payment methods differ in behaviour, not just in parameters, so the behaviour ends up expressed in a configuration language nobody can test or debug. The change becomes untyped and unreviewable rather than absent. Reach for configuration when the variation is genuinely data, such as a fee percentage, and never for control flow.
When this is the wrong answer
Reverse the judgement when the extension point is not yours. If third parties must add payment methods without modifying your code, the interface is the product and you build it on day one, with two implementations, because you cannot ship a switch to someone else's repository. The same applies when the switch sits behind a compiled library boundary your consumers cannot patch, or when regulation requires a new method to ship without a release of the core system. In each case the axis of change is known in advance and the abstraction is paid for by a requirement rather than a prediction.
Common weak answers
- "Principles are absolute." A review that cites a principle without naming the change it protects against cannot be argued with, which is the sign it has stopped being engineering.
- "We will refactor later." True and insufficient. Say what triggers it: the third case, the first external implementer, or the first time two branches diverge in shape.