Update (10/21/2009): There is now a second part to this post, which can be found here.
Using data transfer objects (DTO's) is a common technique for transferring data between different modules of an application. Especially within distributed systems DTO's are of great help for transporting data over remote boundaries.
Classic DTO's are basically standard JavaBeans, having a getter/setter pair for each attribute. In larger applications usually quite a lot of DTO's are required, and implementing them is a tedious and time consuming task.
That's why the idea of a generic DTO was born basically a key/value map, which can store any sort of attribute. This approach saves a lot of time for writing dedicated DTO's, but it comes at the cost of losing type safety at compile time.
A type-safe generic DTO
This problem can be solved by identifying attributes not with plain strings, but with unique parametrized identifiers. To do so, let's define a parametrized class Attribute as follows:
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 |
public class Attribute<T> implements Serializable { private static final long serialVersionUID = 1L; private final String name; private Attribute(String name) { if (name == null) { throw new IllegalArgumentException(); } this.name = name; } public static <S> Attribute<S> getInstance(String name) { return new Attribute<S>(name); } @Override public String toString() { return name; } //equals(), hashCode() ... } |
The type parameter <T> defines the data type of a given attribute. For each required attribute a constant identifier should be created, e.g. in form of static members of an interface:
1 2 3 4 5 6 7 |
public interface CustomerAttributes { public final static Attribute<String> NAME = Attribute.getInstance("CustomerAttributes.Name"); public final static Attribute<Integer> ID = Attribute.getInstance("CustomerAttributes.Id"); } |
Note, that Attribute instances are retrieved using a static factory method instead of directly invoking the constructor. This allows for the type of <T> of the created Attribute being determined automatically by the compiler by using type argument inference.
Now let's have a look a the generic DTO itself:
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 |
public class GenericDTO implements Serializable { private static final long serialVersionUID = 1L; private Map<Attribute<?>, Object> attributes = new LinkedHashMap<Attribute<?>, Object>(); public <T> GenericDTO set(Attribute<T> identifier, T value) { attributes.put(identifier, value); return this; } public <T> T get(Attribute<T> identifier) { @SuppressWarnings("unchecked") T theValue = (T) attributes.get(identifier); return theValue; } public <T> T unset(Attribute<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<?>> getAttributes() { return attributes.keySet(); } public boolean contains(Attribute<?>... identifiers) { for (Attribute<?> oneIdentifier : identifiers) { if (!attributes.containsKey(oneIdentifier)) { return false; } } return true; } @Override public String toString() { return "GenericDTO [" + attributes + "]"; } //equals(), hashCode() ... } |
As with the "classic" generic DTO the attributes are stored in a map (by using a LinkedHashMap the toString() method returns the attributes in order of insertion), but the set() and get() methods are parametrized with <T>.
That way for an Attribute<T> only a value of type T can be stored in or retrieved from the DTO. For instance the call
1 |
dto.set(CustomerAttributes.ID, "Bob");
|
would yield in a compiler error, as "Bob" is of type String instead of Integer.
Note that the unchecked casts within the get() and unset() methods are actually safe, as attributes can be put into the map only by using the type-safe set() method.
As convinience the set() method returns the DTO itself. That allows for chaining multiple invocations:
1 |
dto.set(CustomerAttributes.ID, 123).set(CustomerAttributes.NAME, "Bob"); |
Summary
By using unique parametrized attribute identifiers a generic DTO can be created, that ensures type safety when putting attributes into it or retrieving them from it.
But one problem remains: the contract of methods that use GenericDTO as parameter or return value is rather loose. It is not obvious, which attributes are allowed/can be expected within one given GenericDTO instance.
This might be documented using JavaDoc, but actually there is another interesting approach for tackling this problem, which is shown in this post.
No comments:
Post a Comment