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.
A dispatcher conditional plus N inline handler methods the agent must scan together to know what runs for a given command. Adding a command edits two places (dispatcher plus host class), and the host file grows by one handler body for every new command added.
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.
One file per command the agent reads in isolation; the dispatcher reads a registry of commands instead of a switch over IDs, and adding a command is one new class plus one registry entry rather than an edit across every dispatcher branch.
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);}}
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.
Cross-command invariants (e.g., 'every command logs') cannot be statically enforced when handlers are loose methods on the dispatcher. The agent must hold the full handler set in context to verify any cross-command concern; refactoring one handler risks unintended impact on neighbouring branches that share helpers.
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.
Command spreads handler logic across N files; the agent must traverse the registry to know which class handles a given command name. Stringy registry keys defeat static type analysis — typo-driven bugs slip past the type system.
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.
Adding a new command is one new class plus one registry entry; the type checker confirms every Command implements execute, and cross-cutting concerns wrap the registry once instead of being duplicated per dispatch branch.
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.
Many trivial Commands (`class XCommand { execute() { return X(); } }`) raise context cost without buying anything — the agent loads one definition per command name to verify what it does. The pattern's gain materializes when commands carry meaningful state or when cross-cutting concerns reuse the uniform interface.