Generic conversion between java domain models

Suppose we have two domain models: service and backend. Our goal is to write a conversion back and forth between these two models. It would be a straightforward task if both of the models would be available to each other. However, in this case due to an earlier design decision, the service model is used by the backend model but the backend model in not available to service model (unidirectional dependency). In other words, backend model can import service model elements, whereas service model doesn’t have access to backend model.

The conversion from backend to service model is fairly easy. In each backend component you can write a method creating an instance of service model component populated with the data retrived from backend model.

The conversion from service Person to backend ImPerson, could look like this:

class ServiceToBackendConverter {
    public static ImPersonBase convert(PersonBase personBase) {
        if (personBase instanceof Mother) {
            return new ImMother((Mother) personBase);
        }
        throw new ConversionException("Conversion not defined for: " + personBase);
    }
}

The drawback of this implementation is that the more model elements you get, the more instanceofs you need to mantain.
The alternative solution to that could be a generic converter using annotated backend model elements, e.g.:
backend component (ImMother) is annotated to be convertable to appropriate service component (Mother.class). Generic converter stores internally static HashMap with the service class to backend class mapping. The convert method takes as argument an instance of service element, looks up the mapping for corresponding backend element class, looks up the backend element class constructors for the one accepting as parameter an instance of service element, invokes that constructor and returns the new backend element instance. There are two convert methods in generic converter. The one with one argument returns a generic Object that you need to cast to proper backend element type. The second method accepts as a second parameter the type of the converted element so no cast is needed.

// service model (without access to backend model)
// package generics.model.service
class PersonBase {}

class Mother extends PersonBase {
    private final String name;

    public Mother(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

// backend model
// package generics.model.backend
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Convertable {
    Class<?> value();
}

abstract class ImPersonBase<T extends PersonBase> {
    abstract public String introduce();

    abstract public T convert();
}

@Convertable(Mother.class)
class ImMother extends ImPersonBase<Mother> {
    public String imName;

    public ImMother(String imName) {
        this.imName = imName;
    }

    // used by converter
    public ImMother(Mother mother) {
        this.imName = mother.getName();
    }

    public String introduce() {
        return getClass().getName() + ":" + imName;
    }

    public Mother convert() {
        return new Mother(imName);
    }
}

// package generics.converter
public class GenericAnnotatedConverter {
    private static Map<Class<?>, Class<?>> map = new HashMap<Class<?>, Class<?>>();
    static {
        registerAnnotated(ImMother.class);
    }

    public static void main(String[] args) throws Exception {

        ImMother imMother = new ImMother("Ania");

        // conversion from backend to service model
        Mother mother = imMother.convert();

        // conversion from service to backend model
        Object imConvert = GenericAnnotatedConverter.convert(mother);
        System.out.println(((ImMother) imConvert).introduce());

        ImPersonBase imPersonBase = GenericAnnotatedConverter.convert(mother, ImPersonBase.class);
        System.out.println(imPersonBase.introduce());

        imMother = GenericAnnotatedConverter.convert(mother, ImMother.class);
        System.out.println(imMother.introduce());
    }

    public static Object convert(Object serviceInstance) throws Exception {
        Class<?> serviceClass = serviceInstance.getClass();
        Class<?> backendClass = map.get(serviceClass);
        if (backendClass == null)
            throw new ConversionException("Could not find conversion mapping for " + serviceClass.getName()
                    + ". Register annotated backend element.");
        Constructor<?>[] backendConstructors = backendClass.getConstructors();
        for (Constructor<?> backendConstructor : backendConstructors) {
            Class<?>[] backendParameterTypes = backendConstructor.getParameterTypes();
            for (Class<?> backendParameterType : backendParameterTypes) {
                if (backendParameterType.isAssignableFrom(serviceInstance.getClass())) {
                    return backendClass.cast(backendConstructor.newInstance(serviceInstance));
                }
            }
        }
        throw new ConversionException("Could not find backend constructor for class " + backendClass
                + " with argument of type: " + serviceClass.getName());
    }

    public static <T> T convert(Object serviceInstance, Class<T> backendInterface) throws Exception {
        Class<?> serviceClass = serviceInstance.getClass();
        Class<? extends T> backendClass = map.get(serviceClass).asSubclass(backendInterface);
        Constructor<?>[] backendConstructors = backendClass.getConstructors();
        for (Constructor<?> backendConstructor : backendConstructors) {
            Class<?>[] backendParameterTypes = backendConstructor.getParameterTypes();
            for (Class<?> backendParameterType : backendParameterTypes) {
                if (backendParameterType.isAssignableFrom(serviceInstance.getClass())) {
                    return backendClass.cast(backendConstructor.newInstance(serviceInstance));
                }
            }
        }
        throw new ConversionException("Could not find backend constructor for class " + backendClass
                + " of type " + backendInterface.getName() + " with an argument of type: "
                + serviceClass.getName());
    }

    private static void registerAnnotated(Class<?> backendClass) {
        Convertable annotation = backendClass.getAnnotation(Convertable.class);
        Class<?> serviceClass = annotation.value();
        map.put(serviceClass, backendClass);
    }
}

// package generics.exception
class ConversionException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    public ConversionException(String message) {
        super(message);
    }
}
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s