mercredi 4 octobre 2023

Simple Terminal Command Parser Design Issue

BRIEF EXPLANATION.
I am implementing a very simple command parser that requires me to handle a few commands: pw path, mv source destination, help, and some others. The commands always start with the command name, and then there are either 0, 1 or 2 strings after that. I do not exclude that the number of strings may change in the future, but the structure is always the same (as it is now).

I thought I could use the command pattern for this. Everything seems good in my schema. However, when I attempted to implement it, I realized that there's a flaw.

I currently pass the necessary informations to the constructors of each Command (each command has its own class as the design pattern suggests). For example, the CdCommand constructor takes the FileSystem and a String Path, the MvCommand takes the FileSystem and String source, String dest, while the HelpCommand only takes the FileSystem. The issue is that I only know the inputs after I have called the constructors. That's because in my design I initialize the HashMap inside CommandExecutionController at the beginning of the program, by hardcoding the command names and passing in the correct command objects.

I could solve this by mandating that all Command classes have a String... parameter in their execute() method. But this would also forse commands that don't require any string (such as the help command) to have this unnedeed parameter.

I am looking for ways to mitigate this issue in practice. I've seen multiple times that the Command Pattern might be a fair solution for this, but haven't yet found any thread that explains what exactly could be done to solve the issue I'm experiencing.

Ideally I would want to follow this pattern and avoid if-else or switches (because they're not scalable), and especially avoid the String... parameter (because not all concrete command classes require it).

ACTUAL CODE.

CommandExecutionController:

public class CommandExecutionController {
    private final HashMap<String, Class<? extends CommandInterface>> commandList;
    private final FileSystemModel fileSystemModel;

    public CommandExecutionController(final FileSystemModel fileSystemModel) {
        this.commandList = new HashMap<>();
        this.fileSystemModel = fileSystemModel;
    }

    public void register(final String commandName, Class<? extends CommandInterface> commandClass) {
        commandList.put(commandName, commandClass);
    }

    public CommandInterface getDispatchedCommand(final String commandName, final String... arguments)
            throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException
    {
        if (commandList.containsKey(commandName)) {
            Class<? extends CommandInterface> command = commandList.get(commandName);
            return command.getConstructor(FileSystemModel.class, String[].class).newInstance(fileSystemModel, arguments);
        }
        throw new IllegalArgumentException("Command NOT found: " + commandName);
    }

CdCommand:

public class CdCommand implements CommandInterface {
    private final String path;
    private final FileSystemModel fileSystemModel;

    public CdCommand(final FileSystemModel fileSystemModel, final String... arguments) {
        this.path = arguments[0];
        this.fileSystemModel = fileSystemModel;
    }

    @Override
    public String execute() {
       return fileSystemModel.cd(path);
    }
}

How I'm currently calling it:

text = text.trim();
        final String[] commandParts = text.split("\\s+");
        final boolean isCommandCorrect = commandParts.length > 0 && checkCommandExistence(commandParts[0]);

        // Test dispatching
        if (commandParts.length > 0) {
            try {
                final CommandInterface command = commandExecutionController.getDispatchedCommand(commandParts[0],
                        String.join(" ", Arrays.copyOfRange(commandParts, 1, commandParts.length))
                );
                return command.execute();
            } catch (NoSuchMethodException | InvocationTargetException | InstantiationException |
                     IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }

I've looked up other stack overflow threads regarding this question and the command pattern, but found no solutions for my current issue.

Aucun commentaire:

Enregistrer un commentaire