2

There is a Message superclass and there are various Message subclasses like WeddingMessage, GreetingMessage, FarewellMessage, Birthday Message.

The Message superclass has a constructor:

public Message(String messageType){
        this.messageType = messageType;
}

The message subclasses all have different constructors, but they all make a call to the superclass, where they pass the messageType as an argument So for example:

public BirthdayMessage( String name, int age){
    super("birthday");
    System.out.println("Happy birthday " + name + "You are " + age " years old");

public FareWellMessage(String name, String message){
    super("farewell");
    System.out.println(message + " " + name);
}

The messageType which is created is determined by arguments passed in by the user. So for example, if a user inserts 'birthday John 12', then a BirthdayMessage will be created with parameters John and 12. If a user enters 'farewell Grace take care' then an instance of FarewellMessage is created with those parameters.

Instead of having a bunch of if/else statements or a switch case, in the form of something like-

words[] = userinput.slice(' ');
word1 = words[0];
if (word1 == birthday)
     create new BirthdayMessage(parameters here)
if (word1 == wedding)
    create new weddingMessage(parameters here)

etc

How could i use reflection to determine which type of Message class to create. My current idea is to use the File class to get all the Files in the package which contain the message subclasses. Then use reflection to get each of their constructor parameter types and see if they match the parameters given by user input. Then make instances of those matching classes with random parameters. When made, the subclass will make a call to its superclass constructor with its messageType. Then i can check to see if the messageType variable matches the user input.

So if the user enters 'birthday john 23' I find all constructors in the package that take a String and an int as parameters and that have a field messageType(inherited from Message). Then i create an instance of that class and check if the messageType is == to the first word in the user input (birthday in this case). If it is, then i create an instance of that class with the user provided parameters.

Is there a better way to do this with reflection?

  • 3
    It would be more complicated than the if/else or switch statements. – user253751 Jan 30 '15 at 23:18
  • will the `parameters here` be different for each message type constructor? – Jose Martinez Jan 30 '15 at 23:22
  • 1
    @JoseMartinez, I believe so. The OP shows 2 examples in the question that seems to be different (BirthdayMessage and FarewellMessage). – wassgren Jan 30 '15 at 23:23
  • If the params will be different than the problem gets harder to solve. There is nothing wrong with the if else statements as long as they are encapsulated nicely in a factory class. – Jose Martinez Jan 30 '15 at 23:24
  • 1
    If the message space is finite you could use an Enum, but I agree with @Pshemo that you should probably just use different format strings and not go class-crazy. – user207421 Jan 31 '15 at 00:38

3 Answers3

1

Wouldn't it be simpler to instead of creating different classes for each message type use different formats which you could store somewhere like Map<String,String>?

I mean something like

Map<String,String> formats = new HashMap<>();
formats.put("birthday","Happy birthday %s. You are %d years old%n");//name, age
formats.put("farewell","%s %s%n");//message, name       

Object[] data = {"Dany", 5};
System.out.printf(formats.get("birthday"),data);

data = new Object[]{"Ferwell Jack.","We will miss you"};
System.out.printf(formats.get("farewell"),data);

If you don't want to recompile your code after each change in formats you can store them in file and load when application starts, or when needed.
Simple way to do it is by java.util.Properties class.

You can create formats.properties file with content like

birthday=Happy birthday %s. You are %d years old%n
farewell=%s %s%n

and code using it could look like

Properties formatProp = new Properties();
formatProp.load(new FileReader("formats.properties"));//        

Object[] data = {"Dany", 5};
System.out.printf(formatProp.getProperty("birthday"),data);

data = new Object[]{"Ferwell Jack.","We will miss you"};
System.out.printf(formatProp.getProperty("farewell"),data);
Pshemo
  • 122,468
  • 25
  • 185
  • 269
  • You are assuming the Message is simply a string, and not a more complex object. For strings this could work but how would this work for (as the OP posts) a class hierarchy? – Joeblade Jan 31 '15 at 01:29
  • @Joeblade I don't assume anything, I propose other approach which would let us have similar functionality with simpler code (without need to create class hierarchy). – Pshemo Jan 31 '15 at 01:33
  • Ok I may be misunderstanding your intent then. The way I read it, you assumed the author wants to only do the out.printf. and nothing more. If that is the only thing that he wants, then yes this would be preferable to reflection / class hierarchy. But this approach will not work if the author wants to do anything else. (say, show message A if age < 10 and messge B if age >= 10, for greeting, or gender based changes, or storage in a db table, or whatever else may be hidden in the class hierarchy). – Joeblade Jan 31 '15 at 01:37
  • @Joeblade "*it seems you are assuming the author wants to only do the out.printf*" I am not assuming that OP *only* wants this functionality, but examples in this question describes only this functionality so I focused my answer on it. If OP will give more info about why my solution is not enough then that is OK, I will remove my answer or upgrade it to something which will take into account other needs of OP, but until then I will leave it as it is now (I like simple solutions so why not show OP one of them?). – Pshemo Jan 31 '15 at 01:47
  • fair enough :) If the OP can be steered away from reflection I will consider that a win by all accounts. – Joeblade Jan 31 '15 at 11:18
1

If you want to go this route (I hate reflection, but it has it's uses) make sure you isolate it inside a factory class. I would recommend looking into @Annotations and mark the classes you want to be scanned with a specific annotation.

Something like: (must admit, actually had fun writing this example)

annotation:

@Target(value = {ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface GreetingcardInstance {
    public String value();
}

Your 2 message classes and base class

public abstract class Message {
    private String name;
    public Message(String name) {
        this.name = name; // not used, but to keep it in line with your example
    }
}

@GreetingcardInstance("birthday")
public class BirthdayMessage extends Message {
    public BirthdayMessage(Integer i) {
        super("birthday");
        // this line prints if this works.
        System.out.println("Birthdaymessage created: " +i);
    }
}

@GreetingcardInstance("other")
public class OtherMessage extends Message{
    public OtherMessage(Integer i, Integer j) {
        super("other");
    }
}

And the factory that hides the nasty reflection code

public class CardFactory {
    private final Map<String, Class> messageClasses;
    public CardFactory() {
        // I had all my implementations of Message in the package instances
        Reflections reflections = new Reflections("instances");
        Set<Class<?>> greetingCardAnnotations = reflections.getTypesAnnotatedWith(GreetingcardInstance.class);
        Map<String, Class> result = new HashMap<String, Class>();
        for (Class c : greetingCardAnnotations) {
            if (Message.class.isAssignableFrom(c)) {
                GreetingcardInstance annotation = (GreetingcardInstance) c.getAnnotation(GreetingcardInstance.class);
                result.put(annotation.value(), c);
            }
        }
        messageClasses = result;
    }

    public Message createMessage(String messageType, Object[] arguments) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class clazz = messageClasses.get(messageType);
        if (clazz == null) {
            throw new IllegalArgumentException("MessageType: " + messageType + " not supported");
        }
        Class[] argumentTypes = new Class[arguments.length];
        for (int i = 0; i < arguments.length; ++i) {
            argumentTypes[i] = arguments[i].getClass();
        }
        Constructor constructor = clazz.getConstructor(argumentTypes);
        return (Message) constructor.newInstance(arguments);
    }
}

You can either use spring or google's library or scan them by hand, though you'll find that's a pain in the behind. In this example I used the google library which works pretty well.

in this particular implementation the classes all live in the same package. I don't think this is too bad but might not work for you.

I've also not handled basic types, the constructors in this case take Integer, not int as I originally intended.

When you parse a string, just parse the arguments into String, INteger, etc. and pass them as an Object[] , whcih will be used as constructor args.

public static void main(String[] argv) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
    CardFactory cf = new CardFactory();
    System.out.println(cf.toString());

    cf.createMessage("birthday", new Object[] { new Integer(0) });
}

output:

Birthdaymessage created: 0
Community
  • 1
  • 1
Joeblade
  • 1,735
  • 14
  • 22
  • Note, this is not threadsafe. This factory should live on the stack somewhere. (though the map won't be written to actively as it would only be done during construction-time, so technically it might be safe) I still felt it was necessary to point this out. replace with a threadsafe map if you want to make this a singleton. also, spring has excellent scanning capabilities but I didn't want to set up a spring project for this example – Joeblade Jan 31 '15 at 01:26
0

There are lots of ways to do what you want. One way would be to learn how to use an injection library like Google Guice. You'll probably get the most mileage out of that in the long run. Another option would be to learn a language like Clojure edit Clojure example added at end.

If you'd like to see a minimal example of what would look like Java, the following class' main will show you how to do that. Basically, it takes a map of String->Classnames (strings), and turns it into a map of String->Class (objects), then a super simple builder method looks up the codeword in the map and constructs a new instance that class and returns it.

The main builds two of them and prints their output. e.g.

I am a bar.
I'm a baz!

Here's the Java program. If you change the package, you'll have to change the classnames in the textConfig variable. The equivalent Clojure code follows.

package foo;

import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

public abstract class Foo {

    public abstract String something(); // the abstract method

    static class Bar extends Foo {  // one subclass
        @Override public String something() {
            return "I am a bar.";
        }
    }

    static class Baz extends Foo { // another subclass
        @Override public String something() {
            return "I'm a baz!";
        }
    }

    public static Class loadClass(String classname) {        
        try { // wrapper for Class.forName that doesn't throw checked exception
            return Class.forName(classname);
        } catch (ClassNotFoundException ex) { 
            throw new IllegalArgumentException(ex);
        }
    }

    public static Map<String, Class> buildConfig(Map<String, String> textConfig) {
        // turn {codeword, classname} into {codeword, class} entries
        // java 8 voodoo follows...
        return textConfig.entrySet().stream().collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> loadClass(e.getValue())));
    }

    public static Foo construct(Map<String, Class> config, String codeword) {
        try { // lookup codeword and return new instance of class
            return (Foo)config.get(codeword).newInstance();
        }
        catch(InstantiationException | IllegalAccessException ex) {
            throw new IllegalArgumentException(ex);
        }
    }

    public static void main(String[] args) {
        // some configuration, you could hardcode this, or even put the
        // data in annoations if you want to be fancy
        Map<String, String> textConfig = new HashMap<>();
        textConfig.put("codeword-bar", "foo.Foo$Bar");
        textConfig.put("codeword-baz", "foo.Foo$Baz");

        // turn your text config into something more immediately useful
        Map<String, Class> config = buildConfig(textConfig);

        // show that it works.
        System.out.println(construct(config, "codeword-bar").something());
        System.out.println(construct(config, "codeword-baz").something());
    }
}

edit

The verbosity of the above code was driving me nuts. So, in case you're interested, here's equivalent code in Clojure.

It puts two functions into a map with the keys :bar and :baz and it looks them up, invokes them and prints the return value.

user=> (def config {:bar (fn [] "I am a bar.") :baz (fn [] "I'm a bar!")})
#'user/config
user=> (println ((:bar config)))
I am a bar.
nil
user=> (println ((:baz config)))
I'm a bar!
nil
BillRobertson42
  • 12,602
  • 4
  • 40
  • 57