On Domain Specific Languages in Java: Method Chaining

I just came along a very interesting blog posting about Domain Specific Languages (DSL) in Java. In his posting, Mohamed Sanaulla nicely discusses three different patterns to define an internal DSL for constructing object graphs: Method Chaining, Nested Functions and Lambda Expressions. In this series, I am going to look into each of them. In this first posting of the series, I am going to look into the Method Chaining pattern.

On Method Chaining

Let’s consider the example code from Mohamed Sanaulla’s excellent blog posting:

Graph()
    .edge()
        .from(a)
        .to(b)
        .weight(12.3)
    .edge()
        .from(b)
        .to(c)
        .weight(10.5);

This is nicely readable, but it creates a problem: You must call weight(double weight) to return from the EdgeBuilder scope to the GraphBuilder scope again. This means that the order of statements is predefined and you cannot write another permutation like this…

Graph()
    .edge()
        .from(a)
        .to(b)
        .weight(12.3)
    .edge()
        .weight(10.5)
        .from(b) // compiler error - unknown method
        .to(c);

In this particular example, it might not matter a lot because the DSL provides just two scopes: GraphBuilder and EdgeBuilder. However, if you add more scopes to the DSL, then this constraint can get confusing. To fix this issue, you need to add two methods:

  • A method to build the product of the current scope and return it. Let’s call it build().
  • A method to inject the product of the current scope into the parent scope and return it. Let’s call it inject().

Given these two methods, the example becomes:

Graph()
    .edge()
        .from(a)
        .to(b)
        .weight(12.3)
        .inject()
    .edge()
        .weight(10.5)
        .from(b)
        .to(c)
        .inject()
    .build();

Note that inject() is a method of EdgeBuilder. It builds the edge and injects it into the parent GraphBuilder, which it also returns. In contrast, build() is a method of GraphBuilder and returns the final product, which is a Graph. With these changes, the example gets a little bit longer, but now you have the freedom of rearranging the order of the statements, which I did when configuring the second edge.

You can think of these two methods as closing the current scope, that is, the scope of EdgeBuilder and GraphBuilder. Because of these closing braces, you can now translate the DSL from Java to XML almost one-to-one:

<graph>
    <edge>
        <from>a</from>
        <to>b</to>
        <weight>12.3</weight>
    </edge> <!-- .inject() -->
    <edge>
        <weight>10.5</weight>
        <from>b</from>
        <to>c</to>
    </edge> <!-- .inject() -->
</graph> <!-- .build() -->

Considering this example, you may notice that these two methods form a generic pattern, which can be expressed with the following functional interfaces:

public interface Builder<Product> {

    Product build();
}
public interface Injection<Target> {

    Target inject();
}

You would apply them to the original GraphBuilder and EdgeBuilder classes like so:

public class GraphBuilder implements Builder<Graph> {

    ...

    public Graph build() { return graph; }
}
public class EdgeBuilder implements Injection<GraphBuilder> {

    ...

    public GraphBuilder inject() { return gBuilder; }
}

You may ask yourself why these two functional interfaces have not been collapsed into one functional interface, given the fact that the signature of their functions are technically equivalent. Let’s have a look at this simplified approach:

public interface Closure<Item> {

    Item close();
}

What’s wrong with this simplified approach is that you can’t use it to define a fluent builder for a recursive data structure, e.g. a tree. When building a tree, you need two methods again, one to build the tree, and another one to inject the tree into the parent tree. Let’s see a broken example:

tree()
    .name(a)
    .tree()
        .name(b)
        .leaf()
            .name(c)
            .close() // inject leaf c into tree b
        .close() // inject tree b into tree a
    .close(); // runtime error - tree a has no parent

As you can see, this would not work because close() can either inject the product into the parent scope or build the product, but not both at the same time. However, using the Builder and Injection interfaces again, we can fix this example:

tree()
    .name(a)
    .tree()
        .name(b)
        .leaf()
            .name(c)
            .inject()
        .inject()
    .build();

The respective builder class for trees could look like this:

public class TreeBuilder implements Builder<Tree>, Injection<TreeBuilder> {

    private TreeBuilder parent;
    private Tree tree;

    ...

    public TreeBuilder inject() {
        parent.addChildTree(build());
        return parent;
    }

    public Tree build() {
        return tree;
    }

    private void addChildTree(Tree tree) {
        ...
    }
}

Note that this class is both a Builder for a Tree and an Injection for itself.

A Real World Example

To explore the power of these abstractions, let’s look at a real world example for TrueLicense. TrueLicense is a product of mine (please accept my apologies for the product placement) which provides comprehensive license management features for Java applications. The following example defines a licensing schema for a software product, alias SUBJECT:

LicenseConsumerManager cm = new V2XmlLicenseManagementContext(SUBJECT)
    .consumer() // return a license consumer context
    .manager() // begin child license consumer manager
        .parent() // begin parent license consumer manager
            .keyStore() // begin keystore parameters
                .loadFromResource(PUBLIC_KEY_STORE_NAME)
                .storePassword(PUBLIC_KEY_STORE_PASSWORD)
                .alias(PUBLIC_CERT_ENTRY_ALIAS)
                .inject() // end key store parameters
            .pbe() // begin password based encryption (PBE) parameters
                .password(PBE_PASSWORD)
                .inject() // end PBE parameters
            .storeInUserNode(Main.class) // store license key in user preferences
            .inject() // end parent license consumer manager
        .ftpDays(FTP_DAYS) // define a Free Trial Period (FTP) in days
        .keyStore() // begin keystore parameters for the FTP
            .loadFromResource(FTP_KEY_STORE_NAME)
            .storePassword(FTP_KEY_STORE_PASSWORD)
            .alias(FTP_KEY_ENTRY_ALIAS)
            .keyPassword(FTP_KEY_ENTRY_PASSWORD)
            .inject() // end key store parameters for the FTP
        .storeInUserNode(TopSecret.class)
        .build(); // end child license consumer manager

This definition starts with creating a license management context and then uses the Method Chaining pattern to configure and build the license consumer manager for the software product SUBJECT. Because the software product grants a Free Trial Period (FTP) to its users, two license consumer managers get arranged in a Chain Of Responsibility pattern: The parent license consumer manager deals with regular (paid) license keys and the child license consumer manager deals with auto-generated FTP license keys. When transitioning through the license key lifecycle, the parent license consumer manager is always tried first (not shown here). This works pretty much like Java’s class loader hierarchy.

Each license consumer manager requires parameters to persist its license key in a store, encrypt/decrypt it with a password and sign/verify it with a private/public key pair, et al. Note the use of inject() to inject the keystore parameters and encryption parameters into the respective license consumer manager and again to inject the parent license consumer manager into the child license consumer manager.

Furthermore, because the builder pattern is stateful, the example utilizes this feature to save the programmer some typing: The child license consumer manager does not define encryption parameters. In this case then, the encryption parameters get inherited from the parent license consumer manager.

Conclusion

The advantages of applying the Method Chaining pattern for constructing object graphs are as follows:

  1. The code is well comprehensible, even if the object graph is complex.
  2. The IDE can assist you with code completion.
  3. The compiler constrains you to the construction of the object graph and ensures type safety.
  4. The builder can validate the object graph at runtime.
  5. The builder can define default values for some properties of the object graph.
  6. The builder can inherit property values in recursive object graphs.

However, there are some disadvantages, too:

  1. With just one chain of method calls, you can only build trees, not any other type of object graphs. For example, to build a cyclic graph you need more than one chain of method calls.
  2. If the builder does more than just plain object construction, e.g. object validation, then this violates the Single Responsibility Principle. In a use case with complex object graphs, it can be challenging to write and test the builder then.
  3. Some care needs to be taken to make the semantics really independent of the order of statements within a scope. Applying the Builder and Injection interfaces helps.

Enjoy!