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.
This commit is contained in:
1 parent
dd1c4dd42f
commit
868dbea8df
12 files changed
+485
-156
No files matched your search
@@ -0,0 +1,178 @@
|
||||
---
|
||||
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
|
||||
```java
|
||||
// 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`
|
||||
```java
|
||||
// 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:
|
||||
```java
|
||||
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
|
||||
```mermaid
|
||||
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
|
||||
Reference in new issue
Block a user