1
\$\begingroup\$

I write a lot of mini apps. Each app has its own InputHandler for input from the keyboard. It's time to make a lib for that purpose - a CommandLineInterface.

NOTE: there exists a follow up Question here

Can you review my code on

  • Design
  • OOP
  • Clean Code
  • Naming (my bad englisch leads to bad naming)

Interface CommandLineInterpreter

Any class that implements this interface can use the CommandLineInterface. M is the application, which implements the CommandLineInterpreter

public interface CommandLineInterpreter<M> { 
    Set<Command<M>> getCommands();
    Response executeCommand(String identifier, List<String> parameter);
}

Class Command

the Command is responsible to map the command typed via CLI into an action (method-call) on your app. To execute the command the Class is generic on M (which is your app) so you can call these app-specific methods.

public abstract class Command<M> {

    private final String identifier;

    public Command(String identifier) {
        this.identifier = identifier;
    }

    public abstract Response execute(M invoked, List<String> parameter);

    public String getIdentifier() {
        return identifier;
    }

    public boolean isIdentifier(String ident) {
        return identifier.equals(ident);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Command command = (Command) o;
        return Objects.equals(identifier, command.identifier);
    }

    @Override
    public int hashCode() {
        return Objects.hash(identifier);
    } 
}

Class Response

Provides information if the execution of an command was successful or not.

public class Response {
    private final String message;
    private final String details;
    private final Type type;

    private Response(Type type, String message, String details) {
        this.type = type;
        this.message = message;
        this.details = details;
    }

    public static Response fail(String message) {
        return fail(message, "");
    }

    public static Response fail(String message, String details) {
        return response(Type.FAILURE, message, details);
    }

    private static Response response(Type type, String message, String details) {
        return new Response(type, message, details);
    }

    public static Response success() {
        return success("ok", "");
    }

    public static Response success(String message) {
        return success(message, "");
    }

    public static Response success(String message, String details) {
        return response(Type.SUCCESS, message, details);
    }

    public boolean failed() {
        return type == Type.FAILURE;
    }

    @Override
    public String toString() {
        return type.toString() + ":" + message + (details.isEmpty() ? "" : " 
Details: " + details);
    }

    private enum Type {SUCCESS, FAILURE}

Class CommandLineInterface

This class handles the user input from the command line. It is itself a CommandLineInterpreter and offers two basic commands: help and exit.

public class CommandLineInterface implements CommandLineInterpreter<CommandLineInterface> {

    private static final String COMMAND_SEPARATOR = " ";
    private final CommandLineInterpreter cli; 
    private final InputStream input;
    private final PrintStream output;
    private boolean isRunning = true;

    public CommandLineInterface(CommandLineInterpreter<?> cli, InputStream input, PrintStream output) {
        if (cli == null || hasPredefinedCommands(cli.getCommands())) {
            throw new RuntimeException("CommandLineInterpreter interface of " + cli + " is not properly implemented");
        }
        this.cli = cli;
        this.input = input;
        this.output = output;
    }

    private boolean hasPredefinedCommands(Set<? extends Command<?>> commands) {
        return !Collections.disjoint(commands, getCommands());
    }

    public void start() {
        Scanner scanner = new Scanner(input);
        showHelp();
        while (isRunning) {
            output.print("$>");
            String command = scanner.nextLine();
            List<String> line = Arrays.asList(command.split(COMMAND_SEPARATOR));
            String identifier = line.get(0);
            List<String> parameters = line.subList(1, line.size());
            if (isExecutableCommand(identifier)) {
                Response response = executeCommand(identifier, parameters);
                if (response.failed()) {
                    showResponse(response);
                }
            } else {
                showHelp();
            }
        }
    }

    private boolean isExecutableCommand(String identifier) {
        for (Command cmd : getAllCommands()) {
            if (cmd.isIdentifier(identifier)) {
                return true;
            }
        }
        return false;
    }

    private void showHelp() {
        Set<Command<?>> commands = getAllCommands();
        output.println("help - these commands are available:");
        commands.forEach(c -> output.printf(" - %s\n", c.getIdentifier()));
    }

    private void showResponse(Response response) {
        output.println(response);
    }

    @Override
    public Set<Command<CommandLineInterface>> getCommands() {
        Set<Command<CommandLineInterface>> cliCommands = new HashSet<>();
        cliCommands.add(new Command<CommandLineInterface>("exit") {
            @Override
            public Response execute(CommandLineInterface commandLineInterface, List<String> parameter) {
                isRunning = false;
                return Response.success();
            }
        });
        cliCommands.add(new Command<CommandLineInterface>("help") {
            @Override
            public Response execute(CommandLineInterface commandLineInterface, List<String> parameter) {
                showHelp();
                return Response.success();
            }
        });
        return cliCommands;
    }

    private Set<Command<?>> getAllCommands() {
        Set<Command<?>> commands = mapCommands(cli.getCommands());
        commands.addAll(getCommands());
        return commands;
    }

    private Set<Command<?>> mapCommands(Set commands) {
        Set<Command<?>> mappedCommands = new HashSet<>();
        for (Object o : commands) mapCommand(o).ifPresent(mappedCommands::add);
        return mappedCommands;
    }

    private Optional<Command<?>> mapCommand(Object o) {
        return (o instanceof Command<?>) ? Optional.of((Command<?>) o) : Optional.empty();
    }

    @Override
    public Response executeCommand(String identifier, List<String> parameter) {
        Optional<Command<CommandLineInterface>> cmd = getCommands().stream().filter(command -> command.isIdentifier(identifier)).findAny();
        if (cmd.isPresent()) {
            return cmd.get().execute(this, parameter);
        } else {
            return cli.executeCommand(identifier, parameter);
        }
    }

}

Example

If you have implemented the interfaces properly you should have easy to use CLI support for your apps:

public static void main(String[] args) {
    ExampleApplication app = new ExampleApplication();//implements CommandLineInterpreter
    CommandLineInterface commandLineInterface = new CommandLineInterface(app, System.in, System.out);
    commandLineInterface.start();
}

Output

this is an example output from an example app that does not much...

help - these commands are available:
 - exampleCommand
 - help
 - exit
$>help
help - these commands are available:
 - exampleCommand
 - help
 - exit
$>exampleCommand p1 p2 p3
i could do some actual work now if i were not a mere example, parameter [p1, p2, p3]
$>exit

Process finished with exit code 0
\$\endgroup\$
  • \$\begingroup\$ @mdfst13 thanks for re-phrasing my english... \$\endgroup\$ – Martin Frank Feb 26 at 19:27
1
\$\begingroup\$

Generics

To execute the command the Class is generic on M (which is your app)

public abstract class Command<M> {
   // ..
   public abstract Response execute(M invoked, List<String> parameter);
   // ..

\$ \$

M is the application, which implements the CommandLineInterpreter

public interface CommandLineInterpreter<M> { 
   Set<Command<M>> getCommands();
   Response executeCommand(String identifier, List<String> parameter);
}

Both quotes are wrong, because M would allow every reference type like Integer or CustomClassXY.

To implement it like in the quote described that M can only be CommandLineInterface you do not need generics..

abstract class Command {
    // ..
     public abstract Response execute(CommandLineInterface invoked, List<String> parameter);
    // ..
}

interface CommandLineInterpreter {
    Set<Command> getCommands();
    Response executeCommand(String identifier, List<String> parameter);
}

After that we can change the redundant methods to map the Commands inside a Set

private Set<Command<?>> getAllCommands() {
   Set<Command<?>> commands = mapCommands(cli.getCommands());
   commands.addAll(getCommands());
   return commands;
}

private Set<Command<?>> mapCommands(Set commands) {
  Set<Command<?>> mappedCommands = new HashSet<>();
  for (Object o : commands) mapCommand(o).ifPresent(mappedCommands::add);
  return mappedCommands;
}

private Optional<Command<?>> mapCommand(Object o) {
   return (o instanceof Command<?>) ? Optional.of((Command<?>) o) : Optional.empty();
}

Can know look like

private Set<Command> getAllCommands() {
    Set<Command> commands = cli.getCommands();
    commands.addAll(getCommands());
    return commands;
}

You can read more about Generics in Effective Java from Joshua Bloch.

A More Suitable Data Structure

// in commandLineInterface.executeCommand
Optional<Command<CommandLineInterface>> cmd = getCommands().stream()
                                                           .filter(command -> command.isIdentifier(identifier))
                                                           .findAny();

The task of executeCommand is to find only one Command from getCommands. The method signature of getCommands is

Set<Command<CommandLineInterface>> getCommands()

A Set only provides its content through an Iterator, so we have to iterate all elements to get the element we want with a time complexity of \$O(n)\$

A Map in the form of Map<Indentifier, Command> is equivalent to a Set but with the benefit that we can access a element through the get-method in \$O(1)\$.

Optional<Command<CommandLineInterface>> cmd = Optional.ofNullable(getCommands().get(identifier));

Feature Envy

The whole point of objects is that they are a technique to package data with the processes used on that data. A classic [code] smell is a method that seems more interested in a class other than the one it is in. The most common focus of the envy is the data.

In Method hasPredefinedCommands

When we take a look into the constructor of CommandLineInterface, we can find a method hasPredefinedCommands.

public CommandLineInterface(CommandLineInterpreter<?> cli, InputStream input, PrintStream output) {
   if (cli == null || hasPredefinedCommands(cli.getCommands())) 

Since the method gets only used at one place and is private we know that the argument commands will always be the commands of a CommandLineInterpreter.

private boolean hasPredefinedCommands(Set<? extends Command<?>> commands) {
   return !Collections.disjoint(commands, getCommands());
}

This method should be renamed to contains and moved into CommandLineInterpreter.

public CommandLineInterface(CommandLineInterpreter<?> cli, InputStream input, PrintStream output) {
    if (cli == null || cli.contains(getCommands()) 

Responsibilities Of CommandLineInterface

You described the CommandLineInterface with

This class handles the user input from the command line. It is itself a CommandLineInterpreter and offers two basic commands: help and exit.

The tagged and in the quote shows that the CommandLineInterface has two responsibilities:

  • handles the user input
  • offers two basic commands

Choose a Different Name Like CommandLineInteraction

The name CommandLineInterface is so abstract, that it could be everything and from a programmer perspective I would thing that this is an interface and not a class.. Maybe the responsibilities for this class makes more sense, if we change the name to ComandLineInteraction.

With this name I associate thinks the already existing method start and but also thinks like stop, clean, isRunning and so on..

Let's Remove the Second Responsibility

First we need to check the reason, why it currently is in CommandLineInterface. In the method CommandLineInterface.getCommands:

public Set<Command<CommandLineInterface>> getCommands() {
 // ..
 cliCommands.add(new Command<CommandLineInterface>("exit") {
   @Override
   public Response execute(CommandLineInterface commandLineInterface, >List<String> parameter) {
     isRunning = false;
     return Response.success();
   }
 });
 // ..
}

The reason is the statement isRunning = false, because isRunning is a private variable of CommandLineInterface and you can only access it inside it.

You already provide a method start and we could create a method stop too. After that the creation of the commands does no longer need to be in CommandLineInterface.

The Method stop

public void stop() {
    isRunning = false;
}

The method showHelp should be public

Inject Default Commands through the Contractor

public CommandLineInteractions(..., Map<String, Command> default) {
    // ...
    this.commands = default;
}

From the outside it now looks like

Map<String, Command<>> defaults  = new HashMap<>();
defaults.put("exit", 
             new Command("exit") {
                 @Override
                 public Response execute(CommandLineInterface commandLineInterface, List<String> parameter) {
                     commandLineInterface.stop();
                     return commandLineInterface.isRunning() 
                         ? Response.fail("App is still running")
                         : Response.success();
             }
});
CommandLineInterface app = new Command(..., defaults);

To The Comments

ok about the proper data type (map instead of set) - i want to free the app of creating a redundant data type (identifier, Command) since the command already provides a method getIdentifier()... so any CommandLineInterpreter has to only return the commands that are availible. the amount of commands is rather short, i don't expect more than a dozends commands to be availible so i tend to CleanCode rule don't optimze , see https://clean-code-developer.com/grades/grade-1-red/#Beware_of_Optimizations

From the link you provide:

Understandability of code hast always priority. Optimized code pretty often is all but well readable.

I think the optimization rule doesn't apply to change the Set to an Map, since the Map is the more readable and intuitive way of getting a Command by its name.

i want to free the app of creating a redundant data type (identifier, Command) since the command already provides a method getIdentifier()...

The identifier can still be the string you used before. So it is not really redundant.

When we philosophize a little bit: Does a command need an identifier (name) by it self?

The Map-solution would make the identifier in the command useless and more flexable:
Imagine you use your tool on work and you have a new Spanish employee with low (in your case German) or English skills. Your tool currently only works in german/english and the new employee don't know how to use it.
With Set solution you need to make a new Command, witch is doing the same only with a new identifier, but with the Map solution you put in the reference of the same command but with a different key.

Create a First Class Collection

i want to free the app

What is a First Class Collection:

Any class that contains a collection should contain no other member variables. Each collection gets wrapped in its own class, so now behaviors related to the collection have a home.

When you know want to switch from a Set to a Map or what ever data-type you need to change at sooo many places. But with a FCC you need only to change inside the FCC.

The FCC would define all the methods you need like get and contains and the outer world doesn't care if you are using inside it a Set or a Map - or short : data encapsulation.

\$\endgroup\$
  • \$\begingroup\$ Hello @Roman i had really problems with generics, i always get "unchecked" warnings - that's why i had so much work around these commands... honestly i have some lacks on generics, which leads into bloated code... (this comment is for the check for instance part) \$\endgroup\$ – Martin Frank Feb 27 at 5:00
  • 1
    \$\begingroup\$ About Responsibilities Of CommandLineInterpreter i was thinking the same. you could (as intented) have a private CommandLineInterpreter who's responsible for the commands, that would make the CommandLineInterface lighter - as well as it would lead to making stop as a public method (mentioned before) \$\endgroup\$ – Martin Frank Feb 27 at 8:20
  • 1
    \$\begingroup\$ Yes, this makes a lot of sense to me :] \$\endgroup\$ – Roman Feb 27 at 8:25
  • \$\begingroup\$ it's weird, i got this idea finally after i posted my code - i wonder, why i never do realize that earlier .... \$\endgroup\$ – Martin Frank Feb 27 at 8:41
  • \$\begingroup\$ aahhhh i did the generics wrong all over!! thank you for pointing that out - this makes all more clear now!! \$\endgroup\$ – Martin Frank Feb 27 at 8:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.