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.
12 comments:
Gunnar, what is the key benefit of it? If it is about not having to write DTOs with getters and setters you now end up writing getters for your attributes like PersonAttributes. It is as tedious as writing DTOs with fields and generate getters/setters.
Unbelievable! Exactly today I searched for a solution like this for a problem at work. Will try it out tomorrow and report back. Thx
Stefan, you don't have to write new getters for every attribute. There is exactly one getter and one setter within GenericDTO, which provide type-safe access for every attribute of every attribute group.
Besides you get meaningful toString(), equals() and hashCode() methods for free.
So when creating a new AttributeGroup you only have to declare each attribute exactly in one place and you are done.
Therefore I think the approach should definitely save some time.
One shortcoming I see is the missing possibility to declare attributes as mandatory by making them final and providing appropriate constructors.
Interesting. I have done essentially the same thing as your original DTO, and ran against a similar problem to the one you addressed here. Cool.
I have two comments/suggestions:
1. An additional way to group attributes is by putting them in Enums. This has the nice benefit of making keys singletons, and making equals be the same as reference equality (==).
2. One feature that I found useful, was the ability to perform operations on values - including putting them in the DTO - without knowing their type. You can use that for enumerating, comparing, and (un)marshalling. In order to do this safely, however, you need to add the run-time type to the attribute, e.g.:
class Attribute {
...
public Attribute(String name, Class type) {
this.name = name;
this.type = type;
}
...
public void validate(Object value) {
if (!type.isAssignableFrom(value.getClass()) { throw ... }
}
...
}
Then, in your DTO class, you can add:
public void validateAndPut(Attribute id, Object value) {
id.validate(value);
attributes.put(id, value);
}
I'm not quite sure how that would play with your attribute groups, though... It might not.
Small but important correction to my previous comment: the "type" field of the Attribute class, and the corresponding constructor arg, have to be Class, not simply Class. This ensures that only objects of the target type will be accepted.
Oh man, I didn't realize Blogger was erasing the "<"s and "&rt;"s in my posts... Here it is again:
The "type" field of the Attribute class, and the corresponding constructor arg, have to be Class<T>, not simply Class. This ensures that only objects of the target type will be accepted.
Gidon, thanks for your feedback.
How would you work with enums as attribute groups? Enums can't be parametrized, so I think you'd loose type-safety.
Could you give a more complete example for your 2nd point? I don't yet fully understand what you've got in mind.
Hi Gunnar,
you're right, enums can't be parametrized, so you might need to roll your own (-- the same way the compiler does it under the hood). Still, it quite simplifies the hierarchy:
interface Attribute<G, T> {}
class PersonAttributes<T> implements Attribute<PersonAttributes<T>, T> {
static final PersonAttributes<String> FIRST_NAME = new PersonAttributes<>();
static final PersonAttributes<Integer> AGE = new PersonAttributes<>();
}
class OrderAttributes<T> implements Attribute<OrderAttributes<T>, T> {
static final OrderAttributes<Long> TOTAL_PRICE = new OrderAttributes<>();
}
A complete working example would be:
interface AttributeGroup<G> {}
interface Attribute<G, T> extends AttributeGroup<G> {}
interface GenericDto<G extends AttributeGroup<G>> {
<A extends Attribute<G, T>, T> T get(A attribute);
<A extends Attribute<G, T>, T> GenericDto<G> set(A attribute, T value);
}
class PersonAttributes<T> implements Attribute<PersonAttributes<?>, T> {
static final PersonAttributes<String> FIRST_NAME = new PersonAttributes<>();
static final PersonAttributes<Integer> AGE = new PersonAttributes<>();
}
GenericDto<PersonAttributes<?>> dto = new GenericDto<>();
Integer age = dto.get(PersonAttributes.AGE);
Note the question marks (<?>) at strategic points.
Minor bug. Your version:
GenericDTO dto = GenericDTO.getInstance(PersonAttributes.class);
Should be:
GenericDTO<PersonAttributes> dto = GenericDTO.getInstance(PersonAttributes.class);
great post
ivanka trump hot pics
Post a Comment