Expose Your Classes Dynamically
Duke and Spire exposing another look... |
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 IDE:s 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 methodMaps.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 ifkey()
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
.
Where is "toMap(Entry::getKey, Entry::getValue)" defined in your example
ReplyDeletethe toMap method is in Collectors::toMap. The imports was not included. Sorry for that.
DeleteThis is really cool, but I'm still trying to wrap my mind around it.
ReplyDeleteI am trying to make an option to return a TreeMap with a String.CASE_INSENSITIVE_ORDER Comparator, and I looked at your other post about the Map.builder() but it's not working for me so far.
Any thoughts?
OK, I figured out how to use the overloaded toMap that takes a Supplier argument. Thank you!
DeleteWhy does InitialKeyBuilder have a build() method? I commented it out and everything seems to be working fine. Is it there by mistake?
ReplyDeleteIf you call the builder without entering any key, then you shall get an empty Map as described in the main example under the "// Create an empty map". This is the reason.
Delete