Decomposed the monolithic `dispatch()` method into modular components using `CommandPrecondition`, `ResultHandler`, and `UsageValidator`. Replaced inline checks and tightly-coupled logic with composable, testable classes, eliminating `instanceof` dependencies. Preserved existing behavior and improved maintainability for future feature additions.
8.8 KiB
sessionId
| sessionId |
|---|
| session-260910-075746-nw8q |
Requirements
Overview & Goals
Refactor CommandProcessor to break its tightly coupled dispatch() method into composable, extensible pieces so that new preconditions, permission strategies, and result handlers can be added without modifying the processor itself.
Scope
In Scope
- Decompose
CommandProcessor.dispatch()into a pipeline of single-responsibility components - Eliminate
instanceofchecks forInteractionContext/SlashCommandContextinside the processor - Preserve all existing behavior and keep the existing test suite green
Out of Scope
- Changes to the
Commandinterface or individual command implementations - Changes to
CommandContextsubclass hierarchy - Changes to
CommandContainer,CommandRegistry, orCommandExecutionResult
Technical Design
Current Implementation
CommandProcessor.dispatch() (~80 lines, CommandProcessor.java:48-129) performs five concerns sequentially:
- Command lookup — resolve command from registry
- Precondition checks — guild-only, slash-only, via
instanceofchecks againstInteractionContext - Permission check —
canExecute()withinstanceof SlashCommandContext - Usage/argument validation —
checkUsage()+checkOptions(), again branching oninstanceof InteractionContext - Execution & result handling — execute command, handle TASK/ERROR/WARN/NOACTION results, catch exceptions
The instanceof checks (lines 66, 197, 220) tightly couple the processor to specific context subtypes, making it impossible to add new context types without modifying the processor.
Key Decisions
- Introduce a
CommandPreconditioninterface — each check (guild-only, slash-only, permissions) becomes a standalone, testable precondition that can be composed into an ordered list. This replaces the inline if-chains and eliminates instanceof checks from the processor. - Move context-type-specific logic into the context itself — instead of
instanceofchecks in the processor, add polymorphic methods toCommandContext(e.g.,supportsSlashOnly(),canSkipPermissionCheck(),validateOptions()). Each context subclass overrides as needed. - Extract result handling into a
ResultHandler— the if-chain for TASK/ERROR/WARN/NOACTION becomes a dedicated component, making it easy to add new result types. - Keep changes incremental — refactor in place without changing the public API of
CommandProcessor(constructor +dispatch()remain the same). The pipeline is internal.
Proposed Changes
1. CommandPrecondition interface
// New file: core/command/precondition/CommandPrecondition.java
public interface CommandPrecondition {
/** Returns null if passed, or an error message to reply with. */
String check(CommandContext context, Command command);
}
Concrete implementations:
GuildOnlyPrecondition— replaces lines 60-64SlashCommandOnlyPrecondition— replaces lines 66-69, usescontext.supportsSlashOnly()instead of instanceofPermissionPrecondition— replacescanExecute()(lines 215-229), usescontext.canSkipPermissionCheck()instead of instanceof
2. Polymorphic methods on CommandContext
Add to CommandContext:
boolean isInteraction()— defaultfalse, overridden totrueinInteractionContextboolean canSkipPermissionCheck()— defaultfalse, overridden inSlashCommandContext
This eliminates all instanceof checks from the processor.
3. CommandResultHandler
// New file: core/command/CommandResultHandler.java
public class CommandResultHandler {
void handle(CommandContext context, CommandExecutionResult result, TaskManager taskManager);
}
Encapsulates the TASK/ERROR/WARN/NOACTION/message reply logic from lines 100-120.
4. Refactored CommandProcessor.dispatch()
The method becomes:
public void dispatch() {
CommandContainer container = resolveCommand();
if (container == null) return;
Command command = container.getCommand();
context.setCommand(command);
configureSentry(command);
for (CommandPrecondition precondition : preconditions) {
String error = precondition.check(context, command);
if (error != null) { context.replyEphemeral(error); return; }
}
if (!usageValidator.validate(context, container)) return;
try {
CommandExecutionResult result = command.execute(context);
resultHandler.handle(context, result, taskManager);
} catch (...) { /* existing error handling */ }
}
Architecture Diagram
graph LR
D[dispatch] --> R[resolveCommand]
R --> P[Preconditions Loop]
P --> GuildOnly
P --> SlashOnly
P --> Permission
P --> UV[UsageValidator]
UV --> E[command.execute]
E --> RH[ResultHandler]
File Structure
| File | Action |
|---|---|
core/command/precondition/CommandPrecondition.java |
New — interface |
core/command/precondition/GuildOnlyPrecondition.java |
New |
core/command/precondition/SlashCommandOnlyPrecondition.java |
New |
core/command/precondition/PermissionPrecondition.java |
New |
core/command/CommandResultHandler.java |
New |
core/command/UsageValidator.java |
New — extracted from checkUsage()/checkOptions() |
core/command/CommandProcessor.java |
Modified — delegate to above components |
core/context/CommandContext.java |
Modified — add isInteraction(), canSkipPermissionCheck() |
core/context/InteractionContext.java |
Modified — override isInteraction() |
context/command/SlashCommandContext.java |
Modified — override canSkipPermissionCheck() |
CommandProcessorTest.java |
Modified — update construction, add unit tests for new components |
Risks
- Behavioral regression — the refactor must preserve exact reply messages and ordering. Mitigated by the existing 1000+ line test suite.
- Precondition ordering matters — guild-only must run before permission check. The list order is explicit and documented.
Testing
Validation Approach
- All existing
CommandProcessorTesttests must pass unchanged (behavior preservation). - New unit tests for each precondition, the result handler, and the usage validator in isolation.
Key Scenarios
- Preconditions short-circuit in order (guild-only before permissions)
- Custom precondition can be added to the list and is invoked
- ResultHandler correctly dispatches TASK to TaskManager, logs ERROR/WARN
- UsageValidator handles all subcommand/group/option combinations
Edge Cases
- Null command from container
- Empty precondition list (all checks pass)
- InteractionContext skips option validation in UsageValidator
Delivery Steps
✓ Step 1: Add polymorphic methods to CommandContext hierarchy
CommandContext subclasses expose their capabilities without requiring instanceof checks.
- Add
isInteraction()(defaultfalse) andcanSkipPermissionCheck()(defaultfalse) toCommandContext.java - Override
isInteraction()to returntrueinInteractionContext.java - Override
canSkipPermissionCheck()to returntrueinSlashCommandContext.java - Verify existing tests still compile and pass
✓ Step 2: Extract preconditions into CommandPrecondition interface and implementations
Precondition checks become standalone, composable components.
- Create
CommandPreconditioninterface incore/command/precondition/ - Implement
GuildOnlyPrecondition— checkscommand.isGuildOnly()andcontext.isFromGuild() - Implement
SlashCommandOnlyPrecondition— checkscommand.isSlashCommandOnly()andcontext.isInteraction() - Implement
PermissionPrecondition— replacescanExecute(), usescontext.canSkipPermissionCheck()instead of instanceof - Add unit tests for each precondition
✓ Step 3: Extract UsageValidator and CommandResultHandler
Usage validation and result handling become dedicated classes.
- Extract
checkUsage()andcheckOptions()intoUsageValidator.java, replacinginstanceof InteractionContextwithcontext.isInteraction() - Extract result-handling if-chain (TASK/ERROR/WARN/NOACTION + reply) into
CommandResultHandler.java - Add unit tests for both classes
✓ Step 4: Refactor CommandProcessor to use the new components
CommandProcessor.dispatch() becomes a thin orchestrator delegating to the extracted components.
- Replace inline precondition checks with a
List<CommandPrecondition>loop - Replace
checkUsage()/checkOptions()calls withUsageValidator - Replace result-handling if-chain with
CommandResultHandler - Remove the now-unused private methods (
canExecute,checkUsage,checkOptions) - Keep constructor backward-compatible or provide a builder/factory for custom precondition lists
- Run full
CommandProcessorTestsuite to confirm behavioral equivalence