Minborg

Minborg
Minborg

Saturday, March 5, 2016

Java 8: A Type Safe Map Builder Using Alternating Interface Exposure

Expose Your Classes Dynamically

Duke and Spire exposing another look... 
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 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 method Maps.builder() and the initial view returned allows us to call:
  1. build() that builds an empty Map (like in the second "empty map" example above)
  2. key(K key) that adds a key to the builder and decides the type (=K) for all subsequent keys (like key(1) above)

Once the initial key(K key) is called, another view of the builder appears exposing only:
  1. value(V value) that adds a value to the builder and decides the type (=V) for all subsequent values (like value("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.

6 comments:

  1. Where is "toMap(Entry::getKey, Entry::getValue)" defined in your example

    ReplyDelete
    Replies
    1. the toMap method is in Collectors::toMap. The imports was not included. Sorry for that.

      Delete
  2. This is really cool, but I'm still trying to wrap my mind around it.

    I 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?

    ReplyDelete
    Replies
    1. OK, I figured out how to use the overloaded toMap that takes a Supplier argument. Thank you!

      Delete
  3. Why does InitialKeyBuilder have a build() method? I commented it out and everything seems to be working fine. Is it there by mistake?

    ReplyDelete
    Replies
    1. If 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

Note: Only a member of this blog may post a comment.