Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make a java method with a dynamic number of arguments which is set at compile-time (lombok-like behavior)

I want to make a Message enumeration with each message being on enum type to avoid errors with typos in the message keys. I also want to use parameters (like #{0}) to be able to insert names and more information. To make things a lot easier, I would like to add the method get, that has a dynamic number of (string typed) arguments - one for each parameter that I want to replace. The exact number of arguments should be set at compile-time and is defined by a field of that enum value.

Consider this enumeration:

public enum Message {
    // Written by hand, ignore typos or other errors which make it not compile.

    NO_PERMISSION("no_permission", 0),
    YOU_DIED("you_died", 1),
    PLAYER_LEFT("player_left", 2);

    private String key;
    private int argAmount;

    Message(String key, int argAmount) {
        this.key = key;
        this.argAmount = argAmount;
    }

    public String replace(String... args) {
        String message = get();
        for (int i = 0; i < args.length; i++) {
            message.replace("#{" + i + "}", args[i]);
        }

        return message;        
    }

    public String get() {
        return myConfigFileWrapper.getMessage(key);
    }
}

When I want to retrieve a message, I use Message.YOU_DIED.replace(myInformation). However, I would have to lookup how many arguments the YOU_DIED message takes, and if there are multiple, I would need to take a look at the config file to see which index belongs to which parameter type.

To clarify this, here is an example: The PLAYER_LEFT message is broadcasted to all players and tells them that player x has left with a score of y. In my .lang file, one would find player_left= The player #{0} left with the score #{1}!. In the source code, I will then need to use Message.PLAYER_LEFT.replace(name, score). When my enum is extended now, I probably have more than 100 messages. This means I simply can not remember if the message was The player #{0} left with the score #{1}! or The player #{1} just left!.

My goal is that the compiler automatically throws an error when the get method is not given the exact amount of arguments it needs. This also means that my IDE autocompletion feature will tell me how many arguments to pass.

As you can see, at the moment I am using varargs to inject the variable information into the message. Why I want to take this a step further should be clear by now. I know that this is a kind of luxury feature, but I am only learning and have no one that expects some sort of result at some time.

One approach would be a Message class with tons of subclasses overriding the original get method with a set number of arguments: get(String name, String score). However, this would make a horrible mess with billions of subclasses - one for each message. I didn't even try to create this sort of Message class(es). Also, using this way would require a lot effort to 'create' all the messages and then later to add new ones.

Next, I looked over the reflection API to make this work, but as soon as I figured that reflection wouldn't work for dynamic compile-time methods, I went on. And as far as I know, actually creating new dynamic methods (and that is what I try to do, basically) is not possible especially as one couldn't use them via normal calls because the method wouldn't exist at compile-time.

The only application doing this thing I know so far is Lombok. Lombok uses annotations which are replaced with byte code at compile-time. I took a look into the source code, but just the core itself is pretty big and has cross dependencies everywhere which make it hard to really understand what is going on.

What is the best and easiest way to generate these methods with a dynamic argument number set at compile-time? And how does that said way work?

Code snippets and links to pages with further information are greatly appreciated.

like image 233
Marco Avatar asked May 25 '15 22:05

Marco


3 Answers

You could limit the amount of subclasses by creating one general subclass for each distinct number of parameters:

public class Message {
    public static final Message0Args NO_PERMISSION = new Message0Args("no_permission");
    public static final Message1Arg YOU_DIED = new Message1Arg("you_died");
    public static final Message2Args PLAYER_LEFT = new Message2Args("player_left");

    private String key;
    private int argAmount;

    protected Message(String key, int argAmount) {
        this.key = key;
        this.argAmount = argAmount;
    }

    // Same replace() method, but make it protected
}

With the subclasses being e.g.:

public class Message2Args extends Message {
    public Message2Args(String key) {
        super(key, 2);
    }

    public String replace(String first, String second) {
        return super.replace(first, second);
    }   
}

Note that Message is no longer an enum, but for all practical purposes it works the same way (with some added flexibility such as subclassing), since enum is just syntactic sugar for a class whose only instances are contained in its own public static final fields.

like image 134
Aasmund Eldhuset Avatar answered Nov 08 '22 02:11

Aasmund Eldhuset


The trouble is that even if you know the number of arguments, you still don't know what they should be. Is it Message.PLAYER_LEFT.replace(name, score) or Message.PLAYER_LEFT.replace(score, name)? Or is it maybe Message.PLAYER_LEFT.replace(name, lastLocation)?

To avoid it, you can go one step further and do something like this:

public abstract class Message<T> {

    public static final Message<Void> YOU_DIED = new Message<Void>("You died.") {
        @Override
        public String create(Void arguments) {
            return this.replace();
        }
    };

    public static final Message<Player> PLAYER_LEFT = new Message<Player>("Player %s left with score %d") {
        @Override
        public String create(Player arguments) {
            return this.replace( arguments.getName(), arguments.getScore());
        }
    };

    private Message(String template) {
        this.template = template;
    }

    private final String template;

    protected String replace( Object ... arguments) {
        return String.format( template, arguments );
    }

    public abstract String create(T arguments);
}

Admittedly this is quite verbose, but there are a few things going for it:

  1. All messages are typesafe.
  2. You can (indeed you have to) use higher level objects, which hopefully carry more meaning. While it's difficult to figure out what should go in the two String parameters of Message.PLAYER_LEFT, if the only argument is an object of type Player, the answer is quite obvious.
  3. Further to the above, what if halfway through you want to change the message to display the player's nickname or level too? All you need to modify is the actual message, the callers don't have to know about it.

The big downside of it though is that if you've got complex messages (for example Message.PLAYER_HIT, which should take two Player type parameters), you've got to write wrapper classes for the parameters (in our examples one that encapsulates both players). This can be quite tedious.

like image 22
biziclop Avatar answered Nov 08 '22 03:11

biziclop


Personally, I would approach the problem this way, since I'm a strong-type guy

public interface Message
{

    public static final Message instance = loadInstance();

    String you_died(Player player);

    String player_left(Player player, int score); 

    // etc. hundreds of them
}

// usage
String x = Message.instance.player_left(player, 10);

// one subclass per language
public class Message_jp implements Message
{
    public String you_died(Player player){ return player.lastName + "君,你地死啦死啦"; }
                                           // or whatever way you like to create a String
    // etc.
}

At runtime, you need to load the proper subclass of Message.

static Message loadInstance()
{
    String lang = conf.get("language"); // e.g. "jp"
    Class clazz = Class.forName("Message_"+lang);  // Message_jp.class
    return clazz.newInstance();
}

This approach embeds all messages in class files, which should be fine.

like image 2
ZhongYu Avatar answered Nov 08 '22 01:11

ZhongYu