8
\$\begingroup\$

Last weekend I had an idea: Why not remove dozens of classes by removing all the basic get/set/equals/hashCode implementation of my data object interfaces? I tend to code against interfaces, so I normally end up, for example, with an "Account" interface and at least one implementation (AccountImpl), etc. But most of the time these implementations are horribly boring (member, get, set, equals, hashCode, done), so why not free up the disk space (just joking) and remove the explicit code, replacing it by a dynamic object (I had the idea when reading about project Lombok, which sounds cool, but seems to rely on some "hacks")?

import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class ProxyUtil {

    public static <T> T createDataObject(final Class<T> dataClassInterface) {
        return dataClassInterface.cast(Proxy.newProxyInstance(dataClassInterface.getClassLoader(), new Class[] { dataClassInterface }, new DataInvocationHandler(dataClassInterface)));
    }        
}

class DataInvocationHandler implements InvocationHandler, Serializable {

    private static final long serialVersionUID = 8288151104916240843L;
    private final Map<String, Object> variables = new HashMap<String, Object>();
    private final Class<?> interfaceClass;

    DataInvocationHandler(final Class<?> interfaceClass) {
        this.interfaceClass = interfaceClass;
    }

    DataInvocationHandler(final Class<?> interfaceClass, final Map<String, Object> variables) {
        this(interfaceClass);
        if (variables != null) {
           this.variables.putAll(variables);
        }
    }

    @Override
    public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {

        final String methodName = method.getName();

        if (methodName.startsWith("get")) {
            final String variable = methodName.substring("get".length());
            return this.variables.get(variable);
        } else if (methodName.startsWith("is")) {
            final String variable = methodName.substring("is".length());
            return this.variables.get(variable);
        } else if (methodName.startsWith("set")) {
            final String variable = methodName.substring("set".length());
            return this.variables.put(variable, args[0]);
        } else if ("clone".equals(methodName)) {
            final Map<String, Object> mapClone = (Map<String, Object>) ((HashMap<String, Object>) this.variables).clone();
            return Proxy.newProxyInstance(this.interfaceClass.getClassLoader(), new Class[] { this.interfaceClass }, new DataInvocationHandler(this.interfaceClass, mapClone));
        } else if ("equals".equals(methodName)) {
            return proxy == args[0];
        } else if ("hashCode".equals(methodName)) {
            return proxy.hashCode();
        }
        throw new UnsupportedOperationException(String.format("Method %s is not supported for this object.", methodName));
    }

}

Test case:

interface TestInterface extends Cloneable {

    Object clone();

    String getA();

    void setA(String a);

    int getX();

    void setX(int x);

}

public class ProxyUtilTest {

    @Test
    public void data_object_must_work_correctly() {
        final TestInterface data = ProxyUtil.createDataObject(TestInterface.class);

        data.setA("XYZ");
        data.setX(123);

        Assert.assertThat(data.getA(), Matchers.equalTo("XYZ"));
        Assert.assertThat(data.getX(), Matchers.equalTo(123));

    }

    @Test
    public void data_object_must_be_clonable() {
         final TestInterface data = ProxyUtil.createDataObject(TestInterface.class);

         data.setA("XYZ");
         data.setX(123);

         Assert.assertThat(data, Matchers.instanceOf(Cloneable.class));

         final TestInterface clone = (TestInterface) data.clone();

         data.setA("ABC");
         data.setX(789);

         Assert.assertThat(clone.getA(), Matchers.equalTo("XYZ"));
         Assert.assertThat(clone.getX(), Matchers.equalTo(123));
    }

    @Test
    public void data_object_must_be_serializable() throws ClassNotFoundException, IOException {
         final TestInterface data = ProxyUtil.createDataObject(TestInterface.class);

         data.setA("XYZ");
         data.setX(123);

         Assert.assertThat(data.getA(), Matchers.equalTo("XYZ"));
         Assert.assertThat(data.getX(), Matchers.equalTo(123));

         final TestInterface clone = cloneBySerialization(data);

         Assert.assertThat(clone.getA(), Matchers.equalTo(data.getA()));
         Assert.assertThat(clone.getX(), Matchers.equalTo(data.getX()));
    }

    @Test
    public void data_object_must_show_methods_on_reflection() {

         final TestInterface data = ProxyUtil.createDataObject(TestInterface.class);

         final Method[] dataMethods = data.getClass().getDeclaredMethods();
         final Set<String> dataMethodNames = new HashSet<String>();
         for (final Method dataMethod : dataMethods) {
             dataMethodNames.add(dataMethod.getName());
         }

         final Method[] interfaceMethods = TestInterface.class.getDeclaredMethods();
         for (final Method interfaceMethod : interfaceMethods) {
             Assert.assertTrue(String.format("%s not contained in %s", interfaceMethod, data.getClass()), dataMethodNames.contains(interfaceMethod.getName()));
         }
    } 

    private <T> T cloneBySerialization(final T object) throws IOException, ClassNotFoundException {
         final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
         final ObjectOutputStream output = new ObjectOutputStream(byteArrayOutputStream);
         output.writeObject(object);

         final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
         final ObjectInputStream input = new ObjectInputStream(byteArrayInputStream);
         return (T) input.readObject();
    }

}

This implementation is far from perfect, of course (very far), just something hacked together quickly - and probably has been done dozens of times already (didn't find anything searching quickly, but I assume that this isn't really a new idea). But it seems to work, is serializable (even Cloneable, if the interface is) and shows the methods correctly via reflection, allowing some tools that rely on reflection to work on the object.

Advantages?

  • Less explicit code, only interfaces
  • Same behavior for all data classes
  • Would be a good place to extend for adding more complicated stuff that is required for some use cases (for example, implementing a history for all members would be easy).

Disadvantages?

  • Reflection - Harder to read (but who reads data object code?), slower, harder to debug
  • Who really needs it?
  • Annotations cannot be used directly in the object, just in the interface (which might or might not be a problem, depending on the use case)

At the moment I am unsure. Somehow I think it's cool, and somehow I have the nagging feeling that it's not something you "should" do. But perhaps that's just me worrying. Any comments?

\$\endgroup\$
  • \$\begingroup\$ You would create an interface for each data object class just for the sake of coding against an interface....? \$\endgroup\$ – Edwin Aug 10 '15 at 8:09
  • \$\begingroup\$ I tend to code against interfaces, yes, including beans. For example, for my persistence layer, I wrote the interfaces and then implemented them - including the beans, since the beans created by persistence provider X may have different requirements (annotations, etc.) than beans created by persistence provider Y. I agree that this might be overkill in other cases, true. \$\endgroup\$ – Florian Schaetz Aug 10 '15 at 8:15
  • \$\begingroup\$ I think you might be interested in Project Lombok… Lombok is an annotation-based java enhancement to lessen the impact of boilerplate, which seems to be exactly what you're looking for \$\endgroup\$ – Vogel612 Nov 1 '15 at 13:32
  • \$\begingroup\$ Been there and tried it before, thanks, but the whole system simply screams "hacked" to me. \$\endgroup\$ – Florian Schaetz Nov 1 '15 at 14:01
3
\$\begingroup\$

I have the nagging feeling that it's not something you "should" do.

For starters, getters and setters are often used to provide some kind of field validation, or to return read-only views on the underlying field. More importantly, you will totally lose the ability to make thread-safe immutable classes instances using this approach. Sure, if you realized all you are dealing with are nothing more than a skeleton of getters and setters, this proxy-based approach might seem plausible, but you will be trading flexibility for functionality...

Code-wise, may I suggest that you flip the constructor-chaining of DataInvocationHandler so that the one will the least arguments 'pass-through' to the next?

DataInvocationHandler(final Class<?> interfaceClass) {
    this(interfaceClass, null);
}

DataInvocationHandler(final Class<?> interfaceClass, final Map<String, Object> variables) {
    this.interfaceClass = interfaceClass;
    if (variables != null) {
       this.variables.putAll(variables);
    }
}

It may not be applicable for DataInvocationHandler since it does not have final fields that needs to be set upon object instantiation, but flipping the constructor-chaining lets you centralize all the fields to set in one single constructor, eliminating compiler errors that tell you about fields not being set.

\$\endgroup\$
  • 1
    \$\begingroup\$ Technically, of course, I could expand the class, adding, for example, a state which tells the object that it should, as of now, not accept any setter calls anymore, which would at least remove modifying setter calls (but not modifying the return value of getter calls). I could, of course, also add some Collections.unmodifiableList(...) returns on getter calls for lists, but probably that's not worth the hassle and would need many different factory methods. Technically, I haven't even found a use for that class, since most of the time I have some default implementation of my interfaces ;-) \$\endgroup\$ – Florian Schaetz Sep 12 '15 at 19:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.