I wrote an command line interpreter (CLI) in Java and got some real helpful hints on how to improve.
The cli is responsible to pick up the users input (via keyboard aka command line) into any app that implements the CommandLineInterpreter
.
The app itself offers commands to the CLI that can be executed. If the user types in a proper command the CLI executes that command on the app.
Note: i changes the name from CommandLineInterface
into CommandLineInteraction
to prevent an misinterpreting naiming (i wished i could speak english)
Can you review this code on
- Design
- Clean code
- OO
- Naming (my bad englisch leads to bad naming)
- i'm not sure if i have too much aggregation and hard coupling here
- how do i test this application?
class Command
any command must subclass from this command so the CLI can execute the command when typed in. The command itself is now responsible to call the right methods on it's Application
public abstract class Command<Application> {
private final String identifier;
private final Application application;
public Command(Application application, String identifier) {
this.application = application;
this.identifier = identifier;
}
public Application getApplication() {
return application;
}
public abstract Response execute(List<String> parameter);
public String getIdentifier() {
return identifier;
}
public boolean isIdentifier(String ident) {
return identifier.equals(ident);
}
//lr'dr
//this class overrides equals and hashCode on sole parameter "identifier"
}
class CommandLineInteraction
this class is responsible to take the user input, find the correspondig command and execute it finally... (yes sounds like too much responsibility for one class but it could be rephrased as handleInput
)
public class CommandLineInteraction implements CommandLineInterpreter {
private static final String COMMAND_SEPARATOR = " ";
private static final String COMMAND_PROMPT = "$>";
private final CommandLineInterpreter cli;
private final CommandLineInteractionInterpreter commandLineInteractionInterpreter;
private final InputStream input;
private final PrintStream output;
private boolean isRunning = true;
public CommandLineInteraction(CommandLineInterpreter cli, InputStream input, PrintStream output) {
commandLineInteractionInterpreter = new CommandLineInteractionInterpreter(this);
if (cli == null || commandLineInteractionInterpreter.hasCommandInCommon(cli)) {
throw new RuntimeException("CommandLineInterpreter interface of " + cli + " is not properly implemented");
}
this.cli = cli;
this.input = input;
this.output = output;
}
@Override
public Set<Command> getCommands() {
return commandLineInteractionInterpreter.getCommands();
}
public void start() {
Scanner scanner = new Scanner(input);
showHelp();
while (isRunning) {
output.print(COMMAND_PROMPT);
String line = scanner.nextLine();
List<String> words = Arrays.asList(line.split(COMMAND_SEPARATOR));
String identifier = words.get(0);
List<String> parameters = words.subList(1, words.size());
Optional<Command> command = findCommand(identifier);
if (command.isPresent()) {
Response response = command.get().execute(parameters);
if (response.failed()) {
output.println(response);
}
} else {
showHelp();
}
}
}
private Optional<Command> findCommand(String identifier) {
return getAllCommands().stream().filter(c -> c.isIdentifier(identifier)).findAny();
}
private Set<Command> getAllCommands() {
Set<Command> commands = cli.getCommands();
commands.addAll(commandLineInteractionInterpreter.getCommands());
return commands;
}
public void showHelp() {
Set<Command> commands = getAllCommands();
output.println("help - these commands are available:");
commands.forEach(c -> output.printf(" - %s\n", c.getIdentifier()));
}
public void stop() {
isRunning = false;
}
}
well honestly this class looks now far more readable - but i must confess i still carry the Set
with this class and not the Map
- this issue is still open and not to be mentioned as review finding. I'm still not sure if this really helps in here. maybe sleep over a night or so...
class CommandLineInteractionInterpreter
provides the two baisc command help
and exit
...
public class CommandLineInteractionInterpreter implements CommandLineInterpreter {
private final Set<Command> cliCommands;
CommandLineInteractionInterpreter(CommandLineInteraction commandLineInterface) {
super();
cliCommands = new HashSet<>();
cliCommands.add(new HelpCommand(commandLineInterface));
cliCommands.add(new ExitCommand(commandLineInterface));
}
@Override
public Set<Command> getCommands() {
return cliCommands;
}
boolean hasCommandInCommon(CommandLineInterpreter cip) {
return !Collections.disjoint(cip.getCommands(), getCommands());
}
}
I also changed the responsibility on execution into the command - away from the interpreter. That leads to the new CommandlineInterpreter
whose quite naked now ^^
interface CommandLineInterpreter
public interface CommandLineInterpreter {
Set<Command> getCommands();
}
so noetworhty, the new responsibility on execution lies on the command:
classes ExitCommand & HelpCommand
public class ExitCommand extends Command<CommandLineInteraction> {
public ExitCommand(CommandLineInteraction commandLineInteraction) {
super(commandLineInteraction, "exit");
}
@Override
public Response execute(List<String> parameter) {
getApplication().stop();
return Response.success();
}
}
public class HelpCommand extends Command<CommandLineInteraction> {
public HelpCommand(CommandLineInteraction commandLineInteraction) {
super(commandLineInteraction, "help");
}
@Override
public Response execute(List<String> parameter) {
getApplication().showHelp();
return Response.success();
}
}
Intrepreter
should REALLY reaturn a first-class collection (like aCommandMap
) - that would solve many problems, that still are in that code \$\endgroup\$Set<Command>
a home, it's done but not visible here =) \$\endgroup\$