A Type Safe Java Map Builder Using Alternating Interface Exposure
Learn how you can make your existing and future Java classes more user-friendly and less error prone using the Alternating Interface Exposure scheme.
Join the DZone community and get the full member experience.
Join For FreeExpose Your Classes Dynamically
When I was a Java newbie, I remember thinking that there should be a way of removing or hiding methods in my classes that I did not want to expose. Like overriding a public
method with a private
or something like that (which of corse cannot and should not be possible). Obviously today, we all know that we could achieve the same goal by exposing an interface
instead.
By using a scheme named Alternating Interface Exposure, we could view a class' methods dynamically and type safe, so that the same class can enforce a pattern in which it is supposed to to be used.
Let me take an example. Let's say we have a Map
builder that can be called by successively adding keys and values before the actual Map
can be built. The Alternating Interface Exposure scheme allows us to ensure that we call the key()
method and the value()
exactly the same number of times and that the build()
method is only callable (and seen, for example in the IDE) when there are just as many keys as there are values.
The Alternating Interface Exposure scheme is used in the open-source project Speedment that I am contributing to. In Speedment, the scheme is for example used when building type-safe Tuples that subsequently will be built after adding elements to a TupleBuilder
. This way, we can get a typed Tuple2<String, Integer>
= {"Meaning of Life", 42}, if we write TupleBuilder.builder().add("Meaning of Life").add(42).build()
Using a Dynamic Map Builder
I have written about the Builder Pattern several times in some of my previous posts (e.g. here) and I encourage you to revisit an article on this issue, should you not be familiar with the concept, before reading on.
The task at hand is to produce a Map
builder that dynamically exposes a number of implementing methods using a number of context-dependent interfaces. Furthermore, the builder shall "learn" its key/value types the first time they are used and then enforce the same type of keys and values for the remaining entries.
Here is an example of how we could use the builder in our code once it is developed:
public static void main(String[] args) {
// Use the type safe builder
Map<Integer, String> map = Maps.builder()
.key(1) // The key type is decided here for all following keys
.value("One") // The value type is decided here for all following values
.key(2) // Must be the same or extend the first key type
.value("Two") // Must be the same type or extend the first value type
.key(10).value("Zehn'") // And so on...
.build(); // Creates the map!
// Create an empty map
Map<String, Integer> map2 = Maps.builder()
.build();
}
}
In the code above, once we start using an Integer using the call key(1)
, the builder only accepts additional keys that are instances of Integer
. The same is true for the values. Once we call value("one")
, only objects that are instances of String
can be used. If we try to write value(42)
instead of value("two")
for example, we would immediately see the error in our IDE. Also, most IDEs would automatically be able to select good candidates when we use code completion.
Let me elaborate on the meaning of this.
Initial Usage
The builder is created using the method Maps.builder()
and the initial view returned allows us to call:
build()
that builds an emptyMap
(like in the second "empty map" example above)key(K key)
that adds a key to the builder and decides the type (=K) for all subsequent keys (likekey(1)
above)
Once the initial key(K key)
is called, another view of the builder appears exposing only:
value(V value)
that adds a value to the builder and decides the type (=V) for all subsequent values (likevalue("one")
)
Note that the build()
method is not exposed in this state, because the number of keys and values differ. Writing Map.builder().key(1).build();
is simply illegal, because there is no value associated with key 1
.
Subsequent Usage
Now that the key and value types are decided, the builder would just alternate between two alternating interfaces being exposed depending on if key()
or value()
is being called. If key()
is called, we expose value()
and if value()
is called, we expose both key()
and build()
.
The Builder
Here are the two alternating interfaces that the builder is using once the types are decided upon:
public interface KeyBuilder<K, V> {
ValueBuilder<K, V> key(K k);
Map<K, V> build();
}
public interface ValueBuilder<K, V> {
KeyBuilder<K, V> value(V v);
}
Note how one interface is returning the other, thereby creating an indefinite flow of alternating interfaces being exposed. Here is the actual builder that make use of the alternating interfaces:
public class Maps<K, V> implements KeyBuilder<K, V>, ValueBuilder<K, V> {
private final List<Entry<K, V>> entries;
private K lastKey;
public Maps() {
this.entries = new ArrayList<>();
}
@Override
public ValueBuilder<K, V> key(K k) {
lastKey = k;
return (ValueBuilder<K, V>) this;
}
@Override
public KeyBuilder<K, V> value(V v) {
entries.add(new AbstractMap.SimpleEntry<>(lastKey, v));
return (KeyBuilder<K, V>) this;
}
@Override
public Map<K, V> build() {
return entries.stream()
.collect(toMap(Entry::getKey, Entry::getValue));
}
public static InitialKeyBuilder builder() {
return new InitialKeyBuilder();
}
}
We see that the implementing class implements both of the alternating interfaces but only return one of them depending on if key()
or value()
is called. I have "cheated" a bit by created two initial help classes that take care about the initial phase where the key and value types are not yet decided. For the sake of completeness, the two "cheat" classes are also shown hereunder:
public class InitialKeyBuilder {
public <K> InitialValueBuilder<K> key(K k) {
return new InitialValueBuilder<>(k);
}
public <K, V> Map<K, V> build() {
return new HashMap<>();
}
}
public class InitialValueBuilder<K> {
private final K k;
public InitialValueBuilder(K k) {
this.k = k;
}
public <V> KeyBuilder<K, V> value(V v) {
return new Maps<K, V>().key(k).value(v);
}
}
These latter classes work in a similar fashion as the main builder in the way that the InitialKeyBuilder
returns a InitialValueBuilder
that in turn, creates a typed builder that would be used indefinitely by alternately returning either a KeyBuilder
or a ValueBuilder
.
Conclusions
The Alternating Interface Exposure scheme is useful when you want a type safe and context-aware model of your classes. You can develop and enforce a number of rules for your classes using this scheme. These classes will be much more intuitive to use, since the context-sensitive model and its types propagate all the way out to the IDE. The schema also gives more robust code, because potential errors are seen very early in the design phase. We will see potential errors as we are coding and not as failed tests or application errors.
Published at DZone with permission of Per-Åke Minborg, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments