On my current project, I need to convert values between types that are only known at runtime: the input type is the JSON type of the input, and the output type is defined in a configuration file loaded at runtime.
I came up with a generic solution, but I'm not quite happy with it, because supporting new input types means you need to change existing supported output type classes (which goes against the Open/Closed Principle), and I use a lot of instanceof
and a lot of casting.
Are there better solutions than what I did in terms of maintainability?
I tried to get a kind of minimal example of my code, so here it is:
package org.example.scratch;
import static java.lang.Float.*;
import static java.lang.Integer.*;
import java.time.LocalDate;
import java.time.ZonedDateTime;
import lombok.Value;
class Scratch {
public static void main(String[] args) {
// runtime values
final FieldValue value = StringValue.of("3.14"); // input value
final Converter<?> converter = FloatConverter.of(); // output type
Mapper<?> mapper = Mapper.of(converter);
FieldValue converted = mapper.convert(value);
System.out.println(converted);
}
@Value(staticConstructor = "of")
static class Mapper<T extends FieldValue> {
Converter<T> converter;
public T convert(FieldValue value) {
return converter.apply(value);
}
}
interface Converter<T extends FieldValue> {
T apply(FieldValue value);
}
@Value(staticConstructor = "of")
static class IntegerConverter implements Converter<IntegerValue> {
@Override
public IntegerValue apply(FieldValue value) {
if (value instanceof IntegerValue) {
return (IntegerValue) value;
}
if (value instanceof FloatValue) {
return IntegerValue.of(((FloatValue) value).getValue().intValue());
}
if (value instanceof StringValue) {
return IntegerValue.of(parseInt(((StringValue) value).getValue()));
}
throw new IllegalArgumentException("Impossible to convert a " + value.getClass().getName() + " to a " + IntegerValue.class.getSimpleName());
}
}
@Value(staticConstructor = "of")
static class FloatConverter implements Converter<FloatValue> {
@Override
public FloatValue apply(FieldValue value) {
if (value instanceof FloatValue) {
return (FloatValue) value;
}
if (value instanceof IntegerValue) {
return FloatValue.of(((IntegerValue) value).getValue().floatValue());
}
if (value instanceof StringValue) {
return FloatValue.of(parseFloat(((StringValue) value).getValue()));
}
throw new IllegalArgumentException("Impossible to convert a " + value.getClass().getName() + " to a " + FloatValue.class.getSimpleName());
}
}
@Value(staticConstructor = "of")
static class DateConverter implements Converter<DateValue> {
@Override
public DateValue apply(FieldValue value) {
if (value instanceof DateValue) {
return (DateValue) value;
}
if (value instanceof StringValue) {
return DateValue.of(LocalDate.parse(((StringValue) value).getValue()));
}
throw new IllegalArgumentException("Impossible to convert a " + value.getClass().getName() + " to a " + DateValue.class.getSimpleName());
}
}
@Value(staticConstructor = "of")
static class TimestampConverter implements Converter<TimestampValue> {
@Override
public TimestampValue apply(FieldValue value) {
if (value instanceof TimestampValue) {
return (TimestampValue) value;
}
if (value instanceof StringValue) {
return TimestampValue.of(ZonedDateTime.parse(((StringValue) value).getValue()));
}
throw new IllegalArgumentException("Impossible to convert a " + value.getClass().getName() + " to a " + TimestampValue.class.getSimpleName());
}
}
interface FieldValue {}
@Value(staticConstructor = "of")
static class IntegerValue implements FieldValue {
Integer value;
}
@Value(staticConstructor = "of")
static class FloatValue implements FieldValue {
Float value;
}
@Value(staticConstructor = "of")
static class DateValue implements FieldValue {
LocalDate value;
}
@Value(staticConstructor = "of")
static class TimestampValue implements FieldValue {
ZonedDateTime value;
}
@Value(staticConstructor = "of")
static class StringValue implements FieldValue {
String value;
}
}