In a recent post I presented a generic DTO implementation which allows a type-safe way of accessing its contents.
The presented approach reduces the efforts of writing a lot of dedicated DTOs but comes at the cost that it is not obvious, which attributes are allowed to be put into or can be retrieved from a given DTO instance. Therefore methods using such a generic DTO as parameter or return value define a rather loose contract.
In the following I'm going to show a way to solve this problem. The basic idea is to introduce the concept of "attribute groups". Each attribute belongs to such a group, and each GenericDTO instance is created for exactly one attribute group.
Let's first define a marker interface AttributeGroup from which all concrete attribute groups will derive:
1 2 3 |
public interface AttributeGroup { } |
The Attribute class doesn't differ much from the version introduced in the first post. It's just parametrized with the type of group to which a given attribute belongs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public class Attribute<G extends AttributeGroup, T> implements Serializable { private static final long serialVersionUID = 1L; private String name; public Attribute(String name) { if (name == null) { throw new IllegalArgumentException(); } this.name = name; } public static <F extends AttributeGroup, S> Attribute<F, S> getInstance( String name) { return new Attribute<F, S>(name); } @Override public String toString() { return name; } // equals(), hashCode() ... } |
As in the previous post Attribute instances are declared as constants on interfaces, e.g. PersonAttributes. But now each of these interfaces extends AttributeGroup, while the Attribute instances are parametrized with the attribute group:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public interface PersonAttributes extends AttributeGroup { public static final Attribute<PersonAttributes, String> FIRST_NAME = Attribute.getInstance("FIRST_NAME"); public static final Attribute<PersonAttributes, Integer> AGE = Attribute.getInstance("AGE"); } ... public class OrderAttributes implements AttributeGroup { public static final Attribute<OrderAttributes, Long> TOTAL_PRICE = Attribute.getInstance("TOTAL_PRICE"); } |
Also the GenericDTO class now has a type parameter specifying for which attribute group a given instance is declared:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
public class GenericDTO<G extends AttributeGroup> implements Serializable { private static final long serialVersionUID = 1L; private Class<G> attributeGroup; private Map<Attribute<G, ?>, Object> attributes = new LinkedHashMap<Attribute<G, ?>, Object>(); private GenericDTO(Class<G> attributeGroup) { this.attributeGroup = attributeGroup; } public static <B extends AttributeGroup> GenericDTO<B> getInstance( Class<B> clazz) { return new GenericDTO<B>(clazz); } public <T> GenericDTO<G> set(Attribute<G, T> identifier, T value) { attributes.put(identifier, value); return this; } public <T> T get(Attribute<G, T> identifier) { @SuppressWarnings("unchecked") T theValue = (T) attributes.get(identifier); return theValue; } public <T> T remove(Attribute<G, T> identifier) { @SuppressWarnings("unchecked") T theValue = (T) attributes.remove(identifier); return theValue; } public void clear() { attributes.clear(); } public int size() { return attributes.size(); } public Set<Attribute<G, ?>> getAttributes() { return attributes.keySet(); } public boolean contains(Attribute<G, ?> identifier) { return attributes.containsKey(identifier); } @Override public String toString() { return attributeGroup.getSimpleName() + " [" + attributes + "]"; } // equals(), hashCode() ... } |
That way it is now clear at compile time which attributes are supported by a given GenericDTO instance.
When for instance having a GenericDTO
1 2 3 4 5 6 7 8 9 |
... GenericDTO dto = GenericDTO.getInstance(PersonAttributes.class); //that's fine dto.set(PersonAttributes.FIRST_NAME, "Bob").set(PersonAttributes.AGE, 28); //compiler error dto.set(OrderAttributes.TOTAL_PRICE, 9990); ... |
By introducing the group concept Attribute declaration is a bit more verbose than in the first version, but real compile-time safety and more explicit contracts should be worth the effort, which is still way below than having to implement dedicated DTOs with getters and setters for each attribute as well as proper equals()-, hashCode()- and toString()-Methods.
What do you think is a generic DTO implemented that way a good idea? As always I'm looking forward to receiving your feedback.