Calvin Claus
Calvin Claus

Reputation: 39

String input to specify which function to call [Java] [Best Practice]

The Application

I am writing an application that executes certain functions depending on user input. E.g. if the user input were to be "1 2 add" the output would be "3". I aim to implement many such methods (div, modulo, etc.). As my Scanner recognizes a function name like "add" the function "add()" should be called.

My Way

My way to do this is to let a FunctionHandler class evaluate the input.

Main:

String inputCommand = sc.nextCommand();
functionHandler.handle(inputCommand);

Function Handler:

public class FunctionHandler {

   public void handle (String functionName) {
      if (functionName.equals("add")) {
          add();
      } else if (functionName.equals("div") {
          div();
      }
   }
   private void add() {
   .......
   }
   ....

}

The Problem with that

As I am adding more and more functions the if statement gets very large, and of course the FunctionHandler class too. Also, whenever I add a new function, I have to change code in two places: I have to define the function, and then add the else if clause in handle() to call the function. Which means two pieces of information that should be encapsulated are "stored" completely independent from each other. I was wondering what the best practice was to solve this kind of situation?

My Ideas

I was thinking about using enums, but they don't seem to fit well in this case.

Another idea I had was creating an interface Function, and then a class for each function that implements Function. The interface would have two methods:

  • getName()
  • execute()
  • Then I could create an array (manually) of Functions in the FunctionHandler, through which I could loop to see if the command the user enters matches getName(). However, having a different class for each function is not very clean either, and it also does not get rid of the problem that for each function I am adding I have to do it in two places: the class and the array.

    This question is only about finding out how to solve this problem cleanly. A pointer in the right direction would be appreciated!

    Thanks a lot!

    Upvotes: 2

    Views: 2988

    Answers (5)

    Balázs Édes
    Balázs Édes

    Reputation: 13807

    I would do something like this:

    public class FunctionTest {
        private static final Map<String, Runnable> FUNCTIONS = new HashMap<String, Runnable>() {{
            put("add", () -> System.out.println("I'm adding something!"));
            put("div", () -> System.out.println("I'm dividing something!"));
        }};
    
        public void handle(String functionName) {
            if (!FUNCTIONS.containsKey(functionName)) {
                throw new IllegalArgumentException("No function with this name: " + functionName);
            }
            FUNCTIONS.get(functionName).run();
        }
    }
    

    You basically can use any functional interface in place of Runnable, I used it, because it matches your add() method. You can map the names of the functions to their actual executable instance, get them by name from the Map and execute them.

    You could also create an enum with the desired executable blocks:

    public class FunctionsAsEnumsTest {
        private static enum MyFunction {
            ADD {
                @Override public void execute() {
                    System.out.println("I'm adding something");
                }
            },
            DIV {
                @Override public void execute() {
                    System.out.println("I'm dividing something");
                }
            };
    
            public abstract void execute();
        }
    
        public void handle(String functionName) {
            // #toUpperCase() might not be the best idea, 
            // you could name your enums as you would the methods.
            MyFunction fn = MyFunction.valueOf(functionName.toUpperCase());
            fn.execute();
        }
    }
    

    Upvotes: 0

    Chris Bouchard
    Chris Bouchard

    Reputation: 805

    Another option would be to keep a Map of handlers. If you're using Java 8, they can even be method references.

    // InputType and ResultType are types you define
    Map<String, Function<InputType, ResultType>> operations = new HashMap<>();
    operations.put("add", MathClass::add);
    // ...
    ResultType result = operations.get(userInput).apply(inputObject);
    

    One downside to doing it this way is that your input and output types must be the same for all operations.

    Upvotes: 2

    Erick G. Hagstrom
    Erick G. Hagstrom

    Reputation: 4945

    You could create a custom annotation for the various functions. Then you could employ your array idea, but have it use reflection to discover which functions have your new annotation and what their names are.

    As background, take a look at http://www.oracle.com/technetwork/articles/hunter-meta-2-098036.html and http://www.oracle.com/technetwork/articles/hunter-meta-3-092019.html. They're a bit old, but seem to address the necessary ideas.

    Upvotes: 1

    Bert Peters
    Bert Peters

    Reputation: 1542

    Assuming you do not have a lot of functions that you want to do this way, and do not want to expose yourself to the security risks caused by reflection, you could use a string switch, like this:

    void handleFunction(String function) {
        switch (function) {
            case "foo":
                foo();
                break;
            case "bar":
                bar();
                break;
    
            default:
                throw new IllegalArgumentException("Unknown function " + function);
                break;
        }
    }
    

    Starting Java 7, you can use Strings in a switch statement and the compiler will make something reasonable out of it

    Upvotes: 0

    eugenioy
    eugenioy

    Reputation: 12393

    You can always use reflection if you want a short solution.

    In your handle method you could do something like this:

    Method m = this.getClass().getMethod(functionName, new Class[]{});
    m.invoke(this, new Object[]{});
    

    Upvotes: 0

    Related Questions