javaoverridingabstractimplementation

How to make List logic reusable for various content classes


I have a base abstract class with multiple translate functions of different types, which I want to be able to be overridden. I want to create a generic translator for a List of any of these types. It should look like this:

protected <T> String translate(final List<T> list) {
    if (list == null) {
        return "";
    }
    return conditionWrapper(
            list.stream()
                    // function with the same name for multiple classes as input
                    .map(this::translate)
                    .collect(Collectors.toList())
    );
}

But Java cannot resolve every translate possible here.

The best way I came up with is to make another function:

protected String translateHelper(final Object o) {
    if (o instance of A) {
        return translate((A) o);
    } else if (o instance of B) {
        return translate((B) o);
    } else if ...
    ...
}

But it would be a pain for performance or readability, as there are many classes.

Or I can make all of these classes implement some interface:

protected interface Translatable {
    String translate();
}

But then I will be forced to implement that method inside these classes.
Sometimes you can't even change them, or it would be bad for changing them for a test case.

I need these translate functions, so they can be overridden in subclasses, with every translate function of a class reusing the .translate() method.

protected <T extends Translatable> String translate(final List<T> list) {
    if (list == null) {
        return "";
    }
    return conditionWrapper(
            list.stream()
                    .map(Translatable::translate)
                    .collect(Collectors.toList())
    );
}

protected final class ArgumentTranslator implements Translatable {
    final Argument argument;

    public ArgumentTranslator(final Argument value) {
        this.argument = value;
    }

    @Override
    public String translate() {
        return "something";
    }
}

protected String translate(final Argument argument) {
    return new ArgumentTranslator(argument).translate();
}

translate for a list is called so (which can be simplified with other functions)

this.translate(
        values.argument()
                .stream()
                .map(ArgumentTranslator::new)
                .collect(Collectors.toList())
)

What is better than such a bunch of instanceofs or boilerplate translates?


Solution

  • Make a map that maps specific types to specific implementations of a generalized translate. Assuming you're okay with getting a generics warning you end up @SuppressWarnings ignoring, you can just make the types work fine, too. A tricky aspect of all this is that types can still have subtypes, so you're sort of stuck having to loop through all supertypes with your map. Looks something like this:

    private final Map translators = new HashMap<>(); // intentionally raw.
    
    public <E> void register(Class<E> type, Function<? super E, String> translator) {
      this.translators.put(type, translator);
    }
    
    public String translate(Object o) {
      Class<?> t = o.getClass(); // NPE intentional
      while (t != null) {
        Object translator = translators.get(t);
        if (translator != null) {
          return (String) ((Function) translator).apply(o);
        }
        t = t.getSuperclass();
      }
      throw new IllegalArgumentException("No translator available to translate " + o.getClass()):
    }
    
    register(Integer.class, Integer::toString);
    register(InputStream.class, i -> "Hello, World!");
    register(LocalDate.class, ld -> myFancyDateFormatter.format(ld));
    
    translate(LocalDate.of(2022, 4, 1));
    

    Generics can't do the job of linking together the idea that every key in your translators map is related to a typevar in the associated value, hence you have to eat some generics warnings. The code cannot itself cause heap corruption and all the warnings-generating shenanigans are strictly limited to private aspects of this class, i.e. you @SuppressWarnings those away (and it is safe to do so), and it won't bother the rest of your code base.

    If you want a default 'fallback' translator, no problem! Simply add a translator for Object, e.g. register(Object.class, Object::toString);.

    NB: WARNING - do not use interface types. I suggest you add a check for this (check if the Class<E> type arg is an interface, and throw an IllegalArgumentException). Alternatively, you need to expand the code considerably and use recursion or a queue because any given type needs to add not just its superclass to the queue, but also all its interfaces (which can have multiple parent interfaces and so on. Fortunately you can't have loops in types so you won't have to worry about loops in such structures).