Background:
For a while now I’ve wanted a delegate-like (C# delegates – think of them as basically type-safe function pointers) feature to use in Java (and don’t mention closures or dynamic languages please, I know it will make my whole post obsolete :P), and I tried several attempts to simulate them, but didn’t quite get it right. I learnt about funky uses of the proxy pattern from my ex-team lead (very neat person, hey Steve!) in various projects. Then Java 5 came along…
No wait, before that, one day as I was messing around with cglib (I call it an AOP enabling tech through proxies – go proxy!), I noticed that they provided a dynamic delegate creator, in the cglib sources, see: net.sf.cglib.reflect.MethodDelegate – quoting from Apache Avalon‘s Delegate class, a delegate is defined as an interface with a single method. Note that Avalon has been marked as a closed project – but I don’t think it had anything to do with the delegate definition, phew.
delegates4j is Born:
Anyway, while that was neat (and backward compatible with pre-Java 5 stuff, hmph), it required a method name as a string, not a very big deal, but not quite what I wanted either, I wanted something a little more declarative, and something perhaps simpler, and a little more typesafe; and not requiring an external class enhancer (yes, maybe I just wanted to reinvent the wheel and not use cglib to do it). Annotations to the rescue! So! Keeping in mind that a delegate is an interface with a single method, let’s look at what I have (might want to paste this into an editor if it’s too annoying to read):
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
package net.namingcrisis.delegates4j.core;
import static net.namingcrisis.delegates4j.core.DelegateFactory.cdg; import org.junit.Assert; import org.junit.Before; import org.junit.Test; public class DelegateFactoryBasicTest { // Basic case, also a sampling of how the API is used. @Before public void setUp() throws Exception { } // Delegate type interface, only a single method. // Think of it as a function pointer in disguise // (C# of course supports delegates natively) private static interface IStringProcessor { String process(String s); } // A strategy class with different string processing routines // Does NOT extend StringProcessor, nor do the method names have to match, // this is the point. private static class GString { /* * It's a joke here, but GTK actually has a * gstring type */ private static final String UPPER_CASE = "upper case"; private static final String PRAYER = "prayers"; private static final String EMPTY = "nada"; @Implements(clazz = IStringProcessor.class, mode = UPPER_CASE) public String toUpperCase(String s) { return s.toUpperCase(); } @Implements(clazz = IStringProcessor.class, mode = PRAYER) public String toPrayer(String s) { return "Hallelujah! " + s; } @Implements(clazz = IStringProcessor.class, mode = EMPTY) public String toEmptyString(String s) { return ""; // :P } } // A test client of StringProcessor, assume it to be a simple visitor // Returns a new array of processed strings public String[] printStrings(final String[] strings, IStringProcessor proc) { final String[] res = new String[strings.length]; for (int i = ; i < strings.length; i++) res[i] = proc.process(strings[i]); return res; } @Test public void testCreateDelegate() { final String[] strings = new String[] { "you", "and", "i" }; final GString gs = new GString(); // Dynamically create delegate instance, cdg is a static import // meaning "create delegate", deliberately keeping the name // short and unintrusive. // cdg(interfacetype, mode, implementationInstance) final IStringProcessor upperCaseProcessor = cdg(IStringProcessor.class, GString.UPPER_CASE, gs); String[] res = printStrings(strings, upperCaseProcessor); Assert.assertTrue(res.length == strings.length); for (int i = ; i < strings.length; i++) Assert.assertEquals(res[i], gs.toUpperCase(strings[i])); final IStringProcessor prayerProcessor = cdg(IStringProcessor.class, GString.PRAYER, gs); res = printStrings(strings, prayerProcessor); Assert.assertTrue(res.length == strings.length); for (int i = ; i < strings.length; i++) Assert.assertEquals(res[i], gs.toPrayer(strings[i])); final IStringProcessor emptyProcessor = cdg(IStringProcessor.class, GString.EMPTY, gs); res = printStrings(strings, emptyProcessor); Assert.assertTrue(res.length == strings.length); for (int i = ; i < strings.length; i++) Assert.assertEquals(res[i], gs.toEmptyString(strings[i])); } } |
A couple of things (noted in comments):
- The implementation class did NOT implement the delegate’s interface (or the delegate rather, “delegate interface” is redundant going by the above definition!), it merely provided compatible methods, whose names did not have to match.
- The use of mode, is of course a string, and perhaps not very typesafe either, but it is exclusively there to support the scenario where multiple similar methods implement different behaviour, allowing you to have several strategies in a class, very handy for quick things. Probably not a good idea for larger reusable strategies. If you didn’t have it, just set it as an empty string (at the moment it’ll NPE-out otherwise :P)
- This test doesn’t illustrate it, but to keep things single and fast (method resolution is done at runtime after all), it matches declared methods only, nothing inherited is usable – an interface that extends another but has no method will error out as a delegate type, a class with a superclass annotated method overridden but not annotated, also errors out. (The class bit is redundant, annotations are not inherited anyway, so).
- Finally – also not illustrated by this test, delegates4j supports Java 5 covariance, where the return type of the implementor can be narrower than that of the delegate type.
Hopefully, it is simple enough to use – my initial attempts had multiple steps, but they tried to do more too. An improvement to this is possibly somehow caching resolved methods for reuse keyed by the annotation detail for example, in practice I don’t know if this is worth it yet.
The project is available as Maven downloadable jar (licensed under Apache License v2.0) in my repository, including sources, and tests to better illustrate usage/limitations (and of course, test that it works!). Set the dependency as follows: groupId = net.namingcrisis, artifactId = delegates4j, version = 0.5.
For the curious, under the hood, the only real magic is the use of the JDK Dynamic Proxy to implement the delegate and pass on the request to the supplied instance, and reflection for method resolution – the source is there anyway :-). If you do use it, hate it, try it out, etc. drop me a line anyway :-).
— Kamal