Skip to content

Instantly share code, notes, and snippets.

@sfcgeorge
Last active February 12, 2017 20:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sfcgeorge/f01cc684d1b0ca5cf2b2 to your computer and use it in GitHub Desktop.
Save sfcgeorge/f01cc684d1b0ca5cf2b2 to your computer and use it in GitHub Desktop.
Java class to call methods on objects/classes when given the method name as a String with actual parameters.

DynamicMethodCall

We wrote DynamicMethodCall as a helper class to allow writing wrapper classes in Java by passing methods as data to call later. It allows you to call methods on an object where the method name is passed as a String. This is useful for simplifying anonymous method usage like GUI event handler, for example. It is capable of calling inherited methods, static methods, and polymorphic parameter methods.

Usage:

Call the run() method, passing the object or class you want to call the method on, the name of the method to be called on that class, and optionally any actual parameters to pass to that method.

DynamicMethodCall.run(System.out, "println", "Hello World!");

Example use case implementations

RWT

Swing GUI code is notoriously verbose. To create a button and bind a method to it on click, this is about as concise as you can get it (in Java 7). An anonymous class is used along with instance opening syntax, but it's still 7LOC which I felt was crazy.

new JButton("Example Button") {{
    addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
            this.myMethod();
        }
    });
}};

So I created RWT which allows doing the above in just 1 lines of code, thanks to DynamicMethodCall! The result looks and behaves the same but the code is 7x shorter, more readable, and far easier to remember.

RWT.button("Example Button", this, "myMethod");

Note that Java 8 came out after we created DynamicMethodCall and it includes anonymous methods which shrinks the above Swing code to just 3 lines. It's still not as concise as my RWT solution but probably preferable as the standard way.

new JButton("Example Button") {{
    addActionListener(e -> this.myMethod());
}};

Tester

Imagine you want to write pure Java tests, that both call methods and check the result is as expected, and also print to the terminal the method being tested etc. You have duplication of the method name and the test data. An example of this verbose way:

Game player = new Player("Horatio");
System.out.println("Test: method getName() should equal \"Horatio\".");
System.out.println("Reason: Testing name equals what it was initialized to.");
String result = player.getName();
if (result.equals("Horatio")) {
    System.out.println("Result: Pass.");
}
else {
    System.out.println("Result: Fail. Method returned: " + result);
}

Yuck. We wrote a TestAssertion class using DynamicMethodCall to greatly reduce this code and duplication, resulting in highly readable tests. The chaining is reminiscent of jQuery and the expectations are reminiscent of RSpec.

Game player                 = new Player("Horatio");
TestAssertion testAssertion = new TestAssertion(player);

testAssertion.method("getName")
             .should_equal("Horatio")
             .reason("Testing name equals what it was initialized to.")
             .result(); // runs the above test and printing the expectation and result

Negatives

The code is a little crazy with lots of quirky bits like Class<?> and Object.... It does work in all of our tests for a variety of use cases (instance methods, class methods, inherited methods, methods called with polymorphic parameters, methods expecting primitives but called with wrappers) but you still might not want to use it in production code.

Due to the runtime method lookup this will be necessarily slower than standard code which could have compile-time optimizations. For the use cases above this is not a problem—if a GUI button takes a few milliseconds longer to respond to a click that will be imperceptable to humans; and it is more important that a test suite is simple, readable and correct than fast (plus it isn't consumer facing).

The whole idea is very un-Java-ish. I like dynamic languages so we brutalized Java into one as best as we could. But that probably makes it terrible Java code. Don't use it unless you're as stubborn as I am and love terse code.

import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
/**
* We wrote DynamicMethodCall as a helper class to allow writing wrapper
* classes in Java by passing methods as data to call later.
* It allows you to call methods on an object where the method name is passed
* as a String. This is useful for simplifying anonymous method usage like GUI
* event handler, for example. It is capable of calling inherited methods,
* static methods, and polymorphic parameter methods.
* <p>
* The code within DynamicMethodCall is not very readable and we may not have
* cought all possible bugs yet.
* Thus, we know the code in DynamicMethodCall is complex, but we think in
* this case it is acceptable and justified because it makes code written
* using DynamicMethodCall so much clearer and quicker (to write).
* <p>
* We have put inline coments to try and explain some of the more obscure
* complex bits of Java we have used but it might still not be very
* understandable. Again, as this isn't a core class; we expect it to be used
* by 'client' classes and thus the programmer does not need to understand
* how DynamicMethodCall works internally, just how to use it's implementations.
* <p>
* Usage:<br>
* <br>
* Call ther run() method, passing the object or class you want to call the
* method on, the name of the method to be called on that class, and
* optionally any actual parameters to pass to that method.
* <p>
* <code>
* DynamicMethodCall.run(System.out, "println", "Hello World!");
* </code><br>
* <br>
* See Tester and RWT classes for exampe implementations.
* <p>
* <dl>
* <dt><b>History:</b></dt>
* <dd>
* 1.1.0: Added support for calling inherited methods when called with
* polymorphic actual parameters.<br>
* 1.2.0: Added support for calling static methods on classes.
* </dd>
* <dl>
*
* @author Leddy, K (10493062); George, S (14092969)
* @version 1.2.0
*/
@SuppressWarnings({"unchecked", "serial"}) //We do our own checking
abstract class DynamicMethodCall {
//For converting primitive class types to their wrapper equivalent.
private static HashMap<Class<?>, Class<?>> PRIMITIVES_TO_WRAPPERS =
new HashMap<Class<?>, Class<?>>() {{
put(boolean.class, Boolean.class);
put(byte.class, Byte.class);
put(char.class, Character.class);
put(double.class, Double.class);
put(float.class, Float.class);
put(int.class, Integer.class);
put(long.class, Long.class);
put(short.class, Short.class);
put(void.class, Void.class);
}};
/**
* Call method on object or class with passed actual parameters.
* @param obj the object to call method on.
* @param methodName the method to call.
* @param params the optional varargs params to pass to the method.
*/
public static Object run(Object obj, String methodName, Object... params)
throws Exception {
Method method = null;
Object output = null;
Class<?>[] actualClasses = getActualClasses(params);
method = getPolymorphicMethod(obj, methodName, actualClasses);
if (method == null) {
System.out.println("Can't find this method: NoSuchMethodException");
return null;
}
//Actually invoke the found method
output = method.invoke(obj, params);
return output;
}
/**
* Call method on object or class with passed actual parameters.
* Any exceptions will be caught and ignored.
* @param obj the object to call method on.
* @param methodName the method to call.
* @param params the optional varargs params to pass to the method.
*/
public static Object runSafe(Object obj, String methodName, Object... params) {
try {
return run(obj, methodName, params);
}
catch (Exception e) {
return null;
}
}
private static Class<?>[] getActualClasses(Object[] params) {
Class<?>[] actualClasses = new Class<?>[params.length];
for (int i = 0; i < params.length; i += 1)
//Get classes of all the parameters
actualClasses[i] = params[i].getClass();
return actualClasses;
}
private static Method getPolymorphicMethod(Object obj, String methodName, Class<?>[] actualClasses) {
//Get all of the methods in the object's class
Method[] allMethods;
//Check to see if obj is an Object instance or a Class
if (Class.class.isInstance(obj))
allMethods = ((Class)obj).getMethods();
else
allMethods = obj.getClass().getMethods();
//Loop through all the methods looking for a match
for (Method method : allMethods) {
//If the method names match
if (method.getName().equals(methodName)) {
//Get the types of all parameters
Class<?>[] formalClasses = method.getParameterTypes();
//Because looping starts off assuming a mach, later falsify it
boolean foundMethod = true;
//Loop through the found method's params
for (int i = 0; i < formalClasses.length; i += 1) {
Class<?> actualClass = actualClasses[i];
Class<?> formalClass = formalClasses[i];
//Convert primitives (int) to their wrappers (Integer)
formalClass = formalClass.isPrimitive() ?
PRIMITIVES_TO_WRAPPERS.get(formalClass) :
formalClass;
//Check if parameters match, potentially polymorphically
if (!formalClass.isAssignableFrom(actualClass)) {
//If no match stop & check next method with same name
foundMethod = false;
break;
}
}
if (foundMethod) return method;
}
}
return null;
}
}
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
/**
* Wrapper class around some J* Swing classes to make instantiating them
* and adding action listeners much more concise.
* <p>
* For example, lets say we create a method, and we want to create a button
* that will call that method on click. First we define the method:
* <br><pre>
* private void function myMethod() {
* System.out.println("Hello World!");
* }
* </pre>
* To create a Swing button and add the method to be called on click:
* <br><pre>
* new JButton("Example Button") {{
* addActionListener(new ActionListener() {
* public void actionPerformed(ActionEvent e) {
* this.myMethod();
* }
* });
* }};
* </pre>
* The equivalent button using RWT:
* <br><pre>
* RWT.button("Example Button", this, "myMethod");
* </pre>
* So using Swing directly the code is 7 lines, using Swing wrapped in RWT
* the code is only 1 line. The less code a programmer writes the less places
* there are for bugs to hide, and the more productive the programmer is.
* RWT reduces bugs and increases productivity.
*
* @author Leddy, K (10493062); George, S (14092969)
* @version 1.0.0
*/
@SuppressWarnings("serial")
public abstract class RWT {
/**
* Instantiate a JButton with label
*
* @param label the label to put on the button
* @return a new JButton instance
*/
public static JButton button(String label) {
return new JButton(label);
}
/**
* Instantiate a JButton with label and ActionListener event attached
*
* @param label the label to put on the button
* @param obj the object to call the method on
* @param method the name of the method to call
* @param params the optional parameters to pass to the method
* @return a new JButton instance
*/
public static JButton button(String label, Object obj, String method, Object... params) {
//Params passed to a method in an anonymous class called within a
//static method must be final
final Object fobj = obj;
final String fmethod = method;
final Object[] fparams = params;
//Create new anonymous JButton
return new JButton(label) {{ //Open up the instance to call methods
//Add anonymous action listener class that calls method specified
addActionListener(RWT.e(fobj, fmethod, fparams));
}};
}
/**
* Instantiate a JMenuItem with label
*
* @param label the label to put on the menu item
* @return a new JMenuItem instance
*/
public static JMenuItem menuItem(String label) {
return new JMenuItem(label);
}
/**
* Instantiate a JMenuItem with label and ActionListener event attached
*
* @param label the label to put on the menu item
* @param obj the object to call the method on
* @param method the name of the method to call
* @param params the optional parameters to pass to the method
* @return a new JMenuItem instance
*/
public static JMenuItem menuItem(String label, Object obj, String method, Object... params) {
final Object fobj = obj;
final String fmethod = method;
final Object[] fparams = params;
return new JMenuItem(label) {{
addActionListener(RWT.e(fobj, fmethod, fparams));
}};
}
private static ActionListener e(Object obj, String method, Object... params) {
final Object fobj = obj;
final String fmethod = method;
final Object[] fparams = params;
//Return an anonymous ActionListener instance
return new ActionListener() {
//Override the actionPerformed method called on click
public void actionPerformed(ActionEvent e) {
try {
//Try to run the specified method dynamically
DynamicMethodCall.run(fobj, fmethod, fparams);
}
catch(Exception err) {
err.printStackTrace();
}
}
};
}
}
import java.util.HashMap;
/**
* We wrote TestAssertions as a helper class to make writing tests in Java
* faster, easier, more concise, and less error prone.
* <p>
* The code within TestAssertions is not very readable and we may not have
* cought all possible bugs yet.
* Our reasoning was that while core code (such as CentreIO) should be simple
* and bug free, helper code purely for tests can doesn't have to be so
* strictly coded. This is becaus if the test code breaks the rest of the
* program will be unaffected. You would never run tests in a production
* environment on production data, you would always use test data just in
* case there is a data loss bug in test code. Thus, wwe know the code in
* TestAssertions is complex, but we think in this case it is acceptable
* and justified because it makes test code written using
* TestAssertions so much clearer and quicker.
* <p>
* We have put inline coments to try and explain some of the more obscure
* complex bits of Java we have used but it might still not be very
* understandable. Again, as this isn't a core class we expect it to be used
* by 'client' classes and thus the programmer does not need to understand
* how TestAssertions works internally, just how to use it.
* <p>
* Usage:<br>
* <br>
* You must call always method() first.<br>
* You may optionally call a single should_*() method afterwards.<br>
* You may optionally call reason() at any time.<br>
* You may optionally call results() after calling a should_*() method.<br>
* You may optionally call output() to print the output of the method call.<br>
* You may optionally call stats() once after running all tests to print test statistics CANNOT BE CHAINED.<br>
* You may optionally call return_output() last to return the output of the method call CANNOT BE CHAINED.<br>
* <p>
* To run tests on an object called mathExamples create a new TestAssertions
* object and pass the mathExamples object into the constructor:
* <br><code>
* TestAssertions testAssertions = new TestAssertions(mathExamples);
* </code><br>
* Then to test the method fibonacci(int n) with actual parameter 5 to check if
* the result equals the String "0,1,1,2,3" and print a reason, you would write:
* <code><br>
* testAssertions.method("fibonacci", 5).should_equal("0,1,1,2,3").reason("First 5 elements of the fibonacci sequence").test()
* <br></code>
* <br>
* See Tester class for more exampe usage.
*
* @author Leddy, K (10493062); George, S (14092969)
* @version 1.0.0
*/
@SuppressWarnings({"unchecked", "serial"}) //We do our own checking so warning is unnecesary
public class TestAssertion {
private Object obj;
private Object output;
private String resultString;
private String invokeException;
private int passes;
private int failures;
private int totalTests;
private static HashMap<Class<?>, Class<?>> PRIMITIVES_TO_WRAPPERS =
new HashMap<Class<?>, Class<?>>() {{
put(boolean.class, Boolean.class);
put(byte.class, Byte.class);
put(char.class, Character.class);
put(double.class, Double.class);
put(float.class, Float.class);
put(int.class, Integer.class);
put(long.class, Long.class);
put(short.class, Short.class);
put(void.class, Void.class);
}};;
/**
* Create a TestAssertions object with reference to the passed in object to run test on.
*
* @param obj the object to run tests on.
*/
public TestAssertion(Object obj) {
//This is the object we will call methods on
this.obj = obj;
//Initialise test stats
passes = 0;
failures = 0;
totalTests = 0;
}
/**
* Call the method specified as a String with the parameters passed.
*
* @param methodName The name of the method to be called on obj.
* @param params the (optional) params to be passed to the method to be called.
*/
public TestAssertion method(String methodName, Object... params) {
totalTests += 1;
//build up a string representation of the parameters
String paramsString = "";
for (int i = 0; i < params.length; i += 1) {
paramsString += formatObjectForPrinting(params[i]);
//append a comma to the end of every parameter except the last
if (i != params.length-1) paramsString += ", ";
}
System.out.print("\n" + totalTests + ". " + methodName + "(" + paramsString + ")"); //print name of method we are testing
//Reset state
output = null;
resultString = "(TestAssertion Notice) You didn't call a should_*() method so no expected result to test.";
invokeException = null;
try {
output = DynamicMethodCall.run(obj, methodName, params);
} catch (Exception e) {
//Remaining exception must have come from method itself, store it
invokeException = e.toString();
}
return this; //Always return 'this' to enable chaining
}
private String formatObjectForPrinting(Object unformatted) {
//for primitives, call the default toString() method
if (unformatted == null)
return null;
else if (PRIMITIVES_TO_WRAPPERS.containsValue(unformatted.getClass()))
return unformatted.toString();
//for String objects, surround the string with quotes
else if (unformatted instanceof String)
return "\"" + unformatted + "\"";
//for any object type, use name of class surrounded by square brackets
else
return "[" + unformatted.getClass().getName() + "]";
}
/**
* Test that the result of calling the method was true.
*
* @return this object to enable method chaining.
*/
public TestAssertion should_be_true() {
System.out.print(" should be true.");
should_boolean(true, true);
//should_boolean(false, false); //Equivalent to above
return this;
}
/**
* Test that the result of calling the method was false.
*
* @return this object to enable method chaining.
*/
public TestAssertion should_be_false() {
System.out.print(" should be false.");
should_boolean(false, true);
//should_boolean(true, false); //Equivalent to above
return this;
}
/**
* Test that the result of calling the method was equal to
* the passed parameter.
*
* @param obj the data to test if it was the same as the return
* result of calling method.
* @return this object to enable method chaining.
*/
public TestAssertion should_equal(Object obj) {
System.out.print(" should equal " + formatObjectForPrinting(obj) + ".");
should_boolean(obj, true);
return this;
}
/**
* Test that the result of calling the method was not equal to
* the passed parameter.
*
* @param obj the data to test if it was not the same as the return
* result of calling method.
* @return this object to enable method chaining.
*/
public TestAssertion should_not_equal(Object obj) {
System.out.print(" should not equal " + formatObjectForPrinting(obj) + ".");
should_boolean(obj, false);
return this;
}
/**
* Test that the result of calling the method contains the passed String.
*
* @param obj the string to test if the return result of
* calling method contained it.
* @return this object to enable method chaining.
*/
public TestAssertion should_contain(String string) {
System.out.print(" should contain " + formatObjectForPrinting(string) + ".");
should_contain_boolean(string, true);
return this;
}
/**
* Test that the result of calling the method does not
* contain the passed String.
*
* @param obj the string to test if the return result of
* calling method didn't contain it.
* @return this object to enable method chaining.
*/
public TestAssertion should_not_contain(String string) {
System.out.print(" should contain " + formatObjectForPrinting(string) + ".");
should_contain_boolean(string, false);
return this;
}
private TestAssertion should_contain_boolean(String string, boolean shouldContain) {
boolean success = false;
if (output instanceof String)
success = ((String)output).toLowerCase().contains(string.toLowerCase()) == shouldContain;
else
System.out.println("should_contain() cannot be called on methods that don't return a String");
resultString = success ? null : ("method returned: " + formatObjectForPrinting(output) + ".");
return this;
}
private void should_boolean(Object obj, boolean shouldEqual) {
boolean success;
if (obj instanceof String
|| PRIMITIVES_TO_WRAPPERS.containsValue(obj.getClass()))
success = obj.equals(output) == shouldEqual;
else
success = (obj == output) == shouldEqual;
resultString = success ? null : ("method returned: " + formatObjectForPrinting(output) + ".");
}
/**
* Test that the result of calling the method was null.
*
* @return this object to enable method chaining.
*/
public TestAssertion should_be_null() {
System.out.print(" should be null.");
resultString = (output == null) ?
null :
("method returned: " + formatObjectForPrinting(output) + ".");
return this;
}
/**
* Test that the result of calling the method was not null.
*
* @return this object to enable method chaining.
*/
public TestAssertion should_not_be_null() {
System.out.print(" should not be null.");
resultString = (output != null) ?
null :
("method returned: " + formatObjectForPrinting(output) + ".");
return this;
}
/**
* Test that calling the method did not raise an exception.
*
* @return this object to enable method chaining.
*/
public TestAssertion should_not_raise_exception() {
System.out.print(" should not raise exception.");
resultString = (invokeException == null) ?
null :
("method raised: [" + invokeException + "]");
return this;
}
/**
* Test that calling the method raised an exception.
*
* @return this object to enable method chaining.
*/
public TestAssertion should_raise_exception() {
System.out.print(" should raise exception.");
resultString = (invokeException != null) ?
null :
("method did not raise exception.");
return this;
}
/**
* Print the reason for performing the test.
*
* @return this object to enable method chaining.
*/
public TestAssertion reason(String reason) {
System.out.println("\nReason: " + reason);
return this;
}
/**
* Print the return result of calling the method (useful for debugging).
*
* @return this object to enable method chaining.
*/
public TestAssertion output() {
//Print method output
System.out.println("\nMethod Output: \n" + output);
return this;
}
/**
* Print the results of running the should_*() test.
*
* @return this object to enable method chaining.
*/
public TestAssertion result() {
//Print test result
if (resultString == null) {
System.out.println("Success: 'should' condition met.");
passes += 1;
}
else {
System.out.println("Failed: " + resultString);
failures += 1;
}
return this;
}
/**
* Return the return result of calling the method.
*
* @return the return result of calling the method.
*/
public Object return_output() {
return output;
}
/**
* Print stats about the number of tests run.
*/
public void stats() {
//Not all calls to method() are followed by should_*() and result()
//so totalTests may be > passes + failures.
System.out.println("\n\nTests run: " + totalTests + ", Test Passes: " + passes + ", Test Failures: " + failures);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment