Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the idiomatic way to write common code for a group of classes with identical methods, but not implementing the same interface?

I'm using an external library that provides tightly related classes (generated from some template), but unfortunately without a shared interface, e.g.

public class A {
    public UUID id();
    public Long version();
    public String foo();
    public String bar();
}

public class B {
    public UUID id();
    public Long version();
    public String foo();
    public String bar();
}

public class C {
    public UUID id();
    public Long version();
    public String foo();
    public String bar();
}

// ... and more: D, E, F, etc.

Given I have no influence over the external library, what's the idiomatic way to write logic common to a group of classes that share the same method signatures (at least, for the methods being used by the common logic)?

Currently I do one of three things, on a case-by-case basis:

  1. I write helper methods that take the primitive results from each object, e.g.

    private static void myHelper(UUID id, Long version, String foo, String bar) {
      ...
    }
    

    This way I can "unpack" an object regardless of its type:

    myHelper(whatever.id(), whatever.version(), whatever.foo(), whatever.bar());
    

    But that can get very wordy, especially when I need to work with many members.

  2. In the scenario where I'm only working with getters (i.e. only need to access current values of the objects), I've found a way to use mapping libraries like Dozer or ModelMapper to map A or B or C to my own common class, e.g.

    public class CommonABC {
      UUID id;
      Long version;
      String foo;
      String bar;
    }
    

    By playing with configuration, you can get these libraries to map all members, whether method or field, public or private, to your class, e.g.

    modelMapper.getConfiguration()
        .setFieldMatchingEnabled(true)
        .setFieldAccessLevel(Configuration.AccessLevel.PRIVATE);
    

    But this was kind of a "broadsword" approach, a hack that IMO isn't clearly justified merely to factor out duplicate code.

  3. Finally, in certain other scenarios it was most succinct to simply do

    private static void myHelper(Object extLibEntity) {
      if (extLibEntity instanceof A) {
        ...
      } else if (extLibEntity instanceof B) {
        ...
      } else if (extLibEntity instanceof C) {
        ...
      } else {
        throw new RuntimeException(...);
      }
    }
    

    It's obvious why this is bad.

In enterprise situations where you have to live with a library that is this way, what would you do?

I'm leaning toward writing a very explicit, if verbose, mapper (not using a generic mapper library) that translates these entities from the start. But, I wonder if there's a better way. (Like, is there a way to "cast" an object as implementing a new interface, in runtime?)

like image 456
slackwing Avatar asked May 06 '19 06:05

slackwing


People also ask

What is idiomatic code in programming?

Idiomatic code is code that does a common task in the common way for your language. It's similar to a design pattern, but at a much smaller scale. Idioms differ widely by language. One idiom in C# might be to use an iterator to iterate through a collection rather than looping through it.

What is an idiom in programming language?

5 Answers. Idiomatic code is code that does a common task in the common way for your language. It's similar to a design pattern, but at a much smaller scale. Idioms differ widely by language. One idiom in C# might be to use an iterator to iterate through a collection rather than looping through it.

What is the difference between idiomatic and non-idiomatic ways of writing functions?

So the idiomatic way is the way that matches the style of the other code, non-idiomatic way means you are writing the kind of function but in a different way.

Do all programming languages have the same way of commenting?

Each programming language has a different way of commenting in the source code. PHP and HTML and JavaScript and C# all have slightly different symbols that begin and end code. While there are some language-specific practices, too, there are more shared than not.


2 Answers

An option that is (under the hood) likely similar to the second approach, but comparatively lean and flexible, is to use Dynamic Proxy Classes. With only a few lines of code, you can let any object "appear" to implement a certain interface, as long as it has the required methods. The following is an MCVE that shows the basic approach:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.UUID;

public class DelegatingProxyExample {

    public static void main(String[] args) {

        A a = new A();
        B b = new B();
        C c = new C();

        CommonInterface commonA = wrap(a);
        CommonInterface commonB = wrap(b);
        CommonInterface commonC = wrap(c);

        use(commonA);
        use(commonB);
        use(commonC);
    }

    private static void use(CommonInterface commonInterface) {
        System.out.println(commonInterface.id());
        System.out.println(commonInterface.version());
        System.out.println(commonInterface.foo());
        System.out.println(commonInterface.bar());
    }

    private static CommonInterface wrap(Object object) {
        CommonInterface commonInterface = (CommonInterface) Proxy.newProxyInstance(
            CommonInterface.class.getClassLoader(), 
            new Class[] { CommonInterface.class }, new Delegator(object));
        return commonInterface;
    }

}

// Partially based on the example from
// https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html
class Delegator implements InvocationHandler {

    private static Method hashCodeMethod;
    private static Method equalsMethod;
    private static Method toStringMethod;
    static {
        try {
            hashCodeMethod = Object.class.getMethod("hashCode", (Class<?>[]) null);
            equalsMethod = Object.class.getMethod("equals", new Class[] { Object.class });
            toStringMethod = Object.class.getMethod("toString", (Class<?>[]) null);
        } catch (NoSuchMethodException e) {
            throw new NoSuchMethodError(e.getMessage());
        }
    }

    private Object delegate;

    public Delegator(Object delegate) {
        this.delegate = delegate;
    }

    public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
        Class<?> declaringClass = m.getDeclaringClass();

        if (declaringClass == Object.class) {
            if (m.equals(hashCodeMethod)) {
                return proxyHashCode(proxy);
            } else if (m.equals(equalsMethod)) {
                return proxyEquals(proxy, args[0]);
            } else if (m.equals(toStringMethod)) {
                return proxyToString(proxy);
            } else {
                throw new InternalError("unexpected Object method dispatched: " + m);
            }
        } else {

            // TODO Here, the magic happens. Add some sensible error checks here!
            Method delegateMethod = delegate.getClass().getDeclaredMethod(
                m.getName(), m.getParameterTypes());
            return delegateMethod.invoke(delegate, args);
        }
    }

    protected Integer proxyHashCode(Object proxy) {
        return new Integer(System.identityHashCode(proxy));
    }

    protected Boolean proxyEquals(Object proxy, Object other) {
        return (proxy == other ? Boolean.TRUE : Boolean.FALSE);
    }

    protected String proxyToString(Object proxy) {
        return proxy.getClass().getName() + '@' + Integer.toHexString(proxy.hashCode());
    }
}

interface CommonInterface {
    UUID id();

    Long version();

    String foo();

    String bar();
}

class A {
    public UUID id() {
        return UUID.randomUUID();
    }

    public Long version() {
        return 1L;
    }

    public String foo() {
        return "fooA";
    }

    public String bar() {
        return "barA";
    }
}

class B {
    public UUID id() {
        return UUID.randomUUID();
    }

    public Long version() {
        return 2L;
    }

    public String foo() {
        return "fooB";
    }

    public String bar() {
        return "barB";
    }
}

class C {
    public UUID id() {
        return UUID.randomUUID();
    }

    public Long version() {
        return 3L;
    }

    public String foo() {
        return "fooC";
    }

    public String bar() {
        return "barC";
    }
}

Of course, this uses reflection internally, and should only be used when you know what you're doing. Particularly, you should add some sensible error checking, at the place that is marked with TODO: There, the method of the interface is looked up in the given delegate object.

like image 71
Marco13 Avatar answered Oct 21 '22 05:10

Marco13


The only technique not tried:

package aplus;

public interface Common {
    ...
}

public class A extends original.A implements Common {
}

public class B extends original.B implements Common {
}
like image 22
Joop Eggen Avatar answered Oct 21 '22 04:10

Joop Eggen