8
\$\begingroup\$

In one of my recent projects I faced the problem of transforming an abstract class into another abstract class. The classes were structured like this and are part of an api for questionnaires:

abstract class AnswerConstraint { /* shared implementation for all entities */ }

class LengthConstraint extends AnswerConstraint { /* specific members for this entity */ }
class RangeConstraint extends AnswerConstraint { /* specific members for this entity     */ }

abstract class AnswerConstraintResource { /* base class for serializable entities */ }

class LengthConstraintResource extends AnswerConstraintResource { /* includes members of LengthConstraint that should be serialized */ }
class RangeConstraintResource extends AnswerConstraintResource { /* includes members of RangeConstraint that should be serialized */ }

In my business logic, I only have to deal with the basetype of the entity, but as soon as the entites need to be serialized, I first need to transform them into the according resource entity.

My thoughts were:

  • Create an abstract method in AnswerConstraint for transformation

I do not like this approach because why should my entity deal with serialization classes of the weblayer?

  • Use instanceof to determine concrete type

I do not like this approach because it is not flexible and not object-oriented in any way.

I finally came up with the following solution and would like to hear your opinions about it.

public interface TransformStrategy<S, T> {
    public T transform(S model);
    boolean canTransform(Class<?> clazz);
}

public abstract class AbstractTransformStrategy<S, T> implements TransformStrategy<S, T> {

    private final Class<? extends S> sourceClass;

    public AbstractTransformStrategy(Class<? extends S> sourceClass) {
        this.sourceClass = sourceClass;
    }

    @Override
    public boolean canTransform(Class<?> clazz) {
        return sourceClass.equals(clazz);
    }
}

The AbstractTransformStrategy serves as a base class for all TransformStrategys. Note the constraint (<? extends S>) of the generic type in the constructor argument. I will refer to this later.

The core functionality is implemented in a transformer base class:

public abstract class AbstractModelTransformer<S, T> {

    private final List<TransformStrategy<S, T>> transformStrategies;

    protected AbstractModelTransformer(List<TransformStrategy<S, T>> transformStrategies) {
        this.transformStrategies = transformStrategies;
    }

    public T transform(S model) {
        TransformStrategy<S, T> transformStrategy = transformStrategies.stream().filter(ts -> ts.canTransform(model.getClass())).findFirst().orElseThrow( () -> new IllegalStateException(String.format("No transformer supporting %s could be found", model.getClass().getSimpleName())));
        return transformStrategy.transform(model);
    }
}

An implementation of this could look like this:

public class LengthConstraintTransformStrategy extends AbstractTransformStrategy<AnswerConstraint, AnswerConstraintResource> {
    public LengthConstraintTransformStrategy() {
        super(LengthConstraint.class);
    }

    @Override
    public AnswerConstraintResource transform(AnswerConstraint model) {
        return new LengthConstraintResource(((LengthConstraint) model));
    }
}

Now here the real magic happens. The generic constraint I mentioned earlier forces me to pass a class into the constructor that is a subclass of AnswerConstraint. The implentation of canTransform() in the base class checks the incoming object against this class and therefore, I can safely cast it in the transform() method. Please note that the generic types of the strategy must be equal to the abstract types. I will refer to this again at the end of the post.

The related transformer can look like this:

public class AnswerConstraintModelTransformer extends AbstractModelTransformer<AnswerConstraint, AnswerConstraintResource> {

    private static final List<TransformStrategy<AnswerConstraint, AnswerConstraintResource>> STRATEGIES = new ArrayList<>(
            Arrays.asList(
                    new LengthConstraintTransformStrategy()
            )
    );

    public AnswerConstraintModelTransformer() {
        super(STRATEGIES);
    }
}

Can this code be improved? Maybe somehow avoiding the cast in the transform method?

I initially wanted to put the concrete classes as generic types in the implementation of TransformStrategy to avoid the cast, but that does not work, because the implementation cannot be inserted into the constructor of the transformer. This problem is shortly described with this example (using the classes from above):

class LengthConstraintTransformStrategy implements TransformStrategy<LengthConstraint, LengthConstraintResource> { /* implementation */}

class Main {
    public static void main(String[] args) {

        List<TransformStrategy<AnswerConstraint, AnswerConstraintResource>> list = new ArrayList<>();

        // this fails although AnswerConstraint is the supertype of LengthConstraint and AnswerConstraintResource the supertype of LengthConstraintResource
        list.add(new LengthConstraintTransformStrategy());
    }
}
\$\endgroup\$
  • 3
    \$\begingroup\$ I can see you have put a lot of effort in to writing this post, but to me it looks like example and hypothetical code. Terms like EntityA, Something, Implementation, Base, etc. are all indications that this is not real code in a working project. If you want to discuss abstract ideas, and you can phrase your question in an abstract way (probably without the code at all), then Programmers may be the right place to ask for whether your strategy represents a good design pattern, or not. Code review is for reviewing real implementations, not debating best practices. \$\endgroup\$ – rolfl Oct 21 '14 at 15:28
  • 2
    \$\begingroup\$ The code is used in a working project, although with different class names of cource. I just changed them to avoid confusion about what these classes do. Apparently I failed with this one. I will replace the classnames with the real ones again. (Should not be too much effort with find & replace :) ) The last piece is more of a description of a generics-problem I faced during development and should only summarize the usecase I wanted to implement. \$\endgroup\$ – Thomas Eizinger Oct 21 '14 at 15:51
  • 2
    \$\begingroup\$ If you don't want your constraint instances to have a toResource method (or similar) then I don't think you can avoid the cast. To me it feels an overly complex situation though. A strategy object specifically made to decouple these constraints from the frontend will need a good argument in my opinion, to warrent the indirection. \$\endgroup\$ – Joeblade Oct 29 '14 at 15:21
2
\$\begingroup\$

To me it looks like you are reinventing the wheel.

Several mapping frameworks exist that not only support complex mappings between two similar objects, but also programmatic transformations. For example, you should consider:

\$\endgroup\$

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.