Replace Conditional Dispatcher With Command

Destination
Symptom

A dispatcher method matches a request type or command name against a chain of `if`/`else` branches, each calling a handler method on the same class. The dispatcher grows wider with every new command; the class grows in proportion; the dispatch table and the handlers live tangled in one file.

Goal

Each handler is a Command object with an `execute(payload)` method. The dispatcher holds a registry mapping command name to Command instance; dispatch is a one-line lookup-and-invoke. New commands ship by registering a new Command — the dispatcher never changes.

Before the refactoring

// One processor with a long dispatch conditional plus all handler bodies.
class CommandProcessor {
execute(command, payload) {
if (command === 'create-user') return this.createUser(payload);
if (command === 'delete-user') return this.deleteUser(payload);
if (command === 'reset-password') return this.resetPassword(payload);
if (command === 'update-profile') return this.updateProfile(payload);
throw new Error(`unknown command ${command}`);
}
createUser(payload) { /* ... */ }
deleteUser(payload) { /* ... */ }
resetPassword(payload) { /* ... */ }
updateProfile(payload) { /* ... */ }
}

After the refactoring

// Each handler is a Command object; the registry replaces the conditional.
class CreateUserCommand {
execute(payload) { /* ... */ }
}
class DeleteUserCommand {
execute(payload) { /* ... */ }
}
class ResetPasswordCommand {
execute(payload) { /* ... */ }
}
class UpdateProfileCommand {
execute(payload) { /* ... */ }
}
class CommandProcessor {
constructor() {
this.commands = new Map([
['create-user', new CreateUserCommand()],
['delete-user', new DeleteUserCommand()],
['reset-password', new ResetPasswordCommand()],
['update-profile', new UpdateProfileCommand()],
]);
}
execute(command, payload) {
const handler = this.commands.get(command);
if (!handler) throw new Error(`unknown command ${command}`);
return handler.execute(payload);
}
}
Example source: Illustrative example written for this site, faithful to Kerievsky's pattern shape in Refactoring to Patterns (Addison-Wesley, 2004), chapter 7. Distinct from Replace Conditional Logic With Strategy: Strategy varies *how* one algorithm computes its result; Command varies *which action* is invoked. The book uses an HTTP-style request dispatcher; this JavaScript version keeps that shape.
Pressure

Conditional dispatchers are Shotgun-Surgery magnets — every new command edits the dispatcher and adds a method to the host class. Cross-command invariants (e.g., logging or auth checks) duplicate at every branch; subtle inconsistencies between handlers ship to runtime.

Tradeoff

Command introduces a class per handler; what was one file becomes many. Logical cohesion (handlers that share helpers or share state) must be deliberately re-established via composition. Command queues and undo stacks become natural — useful when needed, ceremony when not.

Relief

Each handler is one cohesive class; the dispatcher is a few lines of generic code. Adding a command is one new file plus one registry entry; tests for a command target one class; cross-cutting concerns (logging, validation) can decorate the dispatcher rather than duplicating per branch.

Trap

A Command class per trivial action becomes ceremony tax — `class FooCommand { execute(x) { return foo(x); } }`. The pattern earns its keep when commands carry state (captured arguments, undo info), when there are many of them, or when cross-cutting concerns benefit from the uniform interface.