Files
netneutrality/.junie/plans/decouple-command-processor.md
T
anthorob 868dbea8df Refactor CommandProcessor to enhance modularity and extensibility
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.
2026-09-10 08:24:21 -04:00

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 instanceof checks for InteractionContext / SlashCommandContext inside the processor
  • Preserve all existing behavior and keep the existing test suite green

Out of Scope

  • Changes to the Command interface or individual command implementations
  • Changes to CommandContext subclass hierarchy
  • Changes to CommandContainer, CommandRegistry, or CommandExecutionResult

Technical Design

Current Implementation

CommandProcessor.dispatch() (~80 lines, CommandProcessor.java:48-129) performs five concerns sequentially:

  1. Command lookup — resolve command from registry
  2. Precondition checks — guild-only, slash-only, via instanceof checks against InteractionContext
  3. Permission check — canExecute() with instanceof SlashCommandContext
  4. Usage/argument validation — checkUsage() + checkOptions(), again branching on instanceof InteractionContext
  5. 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

  1. Introduce a CommandPrecondition interface — 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.
  2. Move context-type-specific logic into the context itself — instead of instanceof checks in the processor, add polymorphic methods to CommandContext (e.g., supportsSlashOnly(), canSkipPermissionCheck(), validateOptions()). Each context subclass overrides as needed.
  3. 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.
  4. 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-64
  • SlashCommandOnlyPrecondition — replaces lines 66-69, uses context.supportsSlashOnly() instead of instanceof
  • PermissionPrecondition — replaces canExecute() (lines 215-229), uses context.canSkipPermissionCheck() instead of instanceof

2. Polymorphic methods on CommandContext

Add to CommandContext:

  • boolean isInteraction() — default false, overridden to true in InteractionContext
  • boolean canSkipPermissionCheck() — default false, overridden in SlashCommandContext

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 CommandProcessorTest tests 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() (default false) and canSkipPermissionCheck() (default false) to CommandContext.java
  • Override isInteraction() to return true in InteractionContext.java
  • Override canSkipPermissionCheck() to return true in SlashCommandContext.java
  • Verify existing tests still compile and pass

✓ Step 2: Extract preconditions into CommandPrecondition interface and implementations

Precondition checks become standalone, composable components.

  • Create CommandPrecondition interface in core/command/precondition/
  • Implement GuildOnlyPrecondition — checks command.isGuildOnly() and context.isFromGuild()
  • Implement SlashCommandOnlyPrecondition — checks command.isSlashCommandOnly() and context.isInteraction()
  • Implement PermissionPrecondition — replaces canExecute(), uses context.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() and checkOptions() into UsageValidator.java, replacing instanceof InteractionContext with context.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 with UsageValidator
  • 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 CommandProcessorTest suite to confirm behavioral equivalence