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 TransformStrategy
s. 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());
}
}
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