SOFTWARE ENGINEERING blog & .lessons_learned
manuel aldana
Manuel Aldana

March 31st, 2008 · No Comments

Fluent interfaces for code comprehensability

Fluent interfaces is a concept that your api is closely designed to the problem area it solves and is written in the target programming language (internal DSL), so there is no language barrier from the view of client code. The goal is to make code more concise, better readable and thus easier to grasp or understand. Martin Fowler already discusses this principle in his blog-entry . For my side I found this principle especially helpful in unit test cases to make test code much more comprehensible and easier to maintain. This entry shows a simple concrete real world example how a fluent interface could look like.

The following example of a fluent api is a little builder to create up directed graphs from the JUNG graph framework. Regarding the graph the typical three subjects are vertices, directed edges and labels. Labels themselves are attached to respective vertices (1:1 relation). Goal of the graph building code was to create graph test-data for unit tests. To be tested functionality was typically code, which was comparing, merging or filtering JUNG graphs.

Verbose, lengthy test-data builder code

The first approach to build-up graphs was code, which was quite procedural. Well, it fulfilled its task to create test data, but it was quite lengthy verbose and cleary showed symptoms of evil duplication. As an example have a look at following test case:


class GraphComparerTest extends TestCase{
 …
  public void testGraphsAreNotEqual() throws Exception {
    Graph graph1 = new DirectedSparseGraph();
    Vertex vertexA = new DirectedSparseVertex();
    graph1.addVertex(vertexA);
    JungHelper.getStringLabeller(graph1).setLabel(vertexA,“A”);
    Vertex vertexB = new DirectedSparseVertex();
    graph1.addVertex(vertexB);
    JungHelper.getStringLabeller(graph1).setLabel(vertexB,“B”);
    Edge edgeAB = new DirectedSparseEdge(vertexA,vertexB);
    graph1.addEdge(edgeAB);   
    Vertex vertexC = new DirectedSparseVertex();
    graph1.addVertex(vertexC);
    JungHelper.getStringLabeller(graph1).setLabel(vertexC,“C”);
    Edge edgeBC = new DirectedSparseEdge(vertexB,vertexC);
    graph1.addEdge(edgeBc);
    //…
    // the same above procedure for graph2, which is second test data for
    // equals test
    //…
    assertFalse(graphComparer.graphsAreEqual(graph1,graph2));     
 }
 …
}

With above code unit tests are just a torture to read. It is far to long and by looking at it you don’t really know where you are (setting up, calling class under test or verification) or what you do (relation of vertices and edges is unclear). Summing up you end up with about 30 lines of code, which is just repeatedly building up graphs as test data. To summarize: This is just ugly code!

Refactoring to fluent interface

To make this client code better, you really need to go to refactor to a better api. Before doing this you need to know the goal, so cry out loudly: “I want my client code to do”:

  1. Adding vertices to graph
  2. Connecting vertices to graph with directed edges
  3. Adding labels to vertices

These three points had to be encapsulated into a little graph builder api. This api should make it possible to fulfill these tasks at best in a one or two liner. So going ahead the the same test as in previous example turned out quite differently:

class GraphComparerTest extends TestCase{
 …
  //intialised in setUp()
  TestGraphBuilder builder;
 
 public void testGraphsAreNotEqual() throws Exception {
    Graph graph1 = builder.addFirstEdge(“A”,“B”).and(“B”,“C”).getBuiltGraph();
    Graph graph2 = builder.addFirstEdge(“A”,“B”).and(“A”,“C”).getBuiltGraph();   
    assertFalse(graphComparer.graphsAreEqual(graph1,graph2));     
 }
 …
}

Looking at this, reading the test case is a real breeze, where builder code shrinked to two lines!! With this you instantly see, that two graphs are created both with two edges, where the second edge is different (connecting “B” with “C” in contrast to “A” with “C”). Now the assertion makes quickly sense, too. The set of edges of both graphs is different so the equals() answer must obviously be false. Looking at a test case and answering such test case questions can be so much fun and further more a big motivation to write more test cases.

Implementation of fluent interface

The client code got shorter, but how does the implementation of this api look like. Here it is:

public class TestGraphBuilder {

  Graph graph;
  //so vertices can be found again and decided whether to create new or just to add edge
  Map<String, Vertex> mappings;

  ///////////////////////////////
  //START: CLIENT API 
  public TestGraphBuilder() {
    resetInternalGraphBuilderState();
  }
   
  //adding edge with initialize (internal edges, vertices get deleted)
  public TestGraphBuilder addFirstEdge(String vertexName1, String vertexName2) throws Exception {
    resetInternalGraphBuilderState();
    return and(vertexName1, vertexName2);
  }

  public TestGraphBuilder and(String vertexName1, String vertexName2) throws Exception {
    addVertexWithLabelToGraph(vertexName1);
    addVertexWithLabelToGraph(vertexName2);
    Edge edge = new DirectedSparseEdge(mappings.get(vertexName1), mappings.get(vertexName2));
    graph.addEdge(edge);
    return this;
  }
   
  public Graph getBuiltGraph() {
    return graph;
  }
  //END: CLIENT API
  ///////////////////////////////
   
   
  private void resetInternalGraphBuilderState() {
    graph = new DirectedSparseGraph();
    mappings = new HashMap<String, Vertex>();
  }

  private void addVertexWithLabelToGraph(String vertexName1) throws StringLabeller.UniqueLabelException {
    if (!mappings.containsKey(vertexName1)) {
      Vertex vertex1 = new DirectedSparseVertex();
      graph.addVertex(vertex1);
      JungHelper.getStringLabeller(graph).setLabel(vertex1, vertexName1);
      mappings.put(vertexName1, vertex1);
    }
  }

}

From client side of view the only interesting methods are the public ones. Internally they initialize/reset things (constructor and addFirstEdge(..)) and further more are adding edges (addFirstEdge(..) and and(..). Finally after finishing buidling you want to get the final graph (getBuiltGraph() The client should be reluctant to create the objects of edges and vertices, it is just interested in the label names, so it is enough to pass them and letting the builder do the boring work. Further more a new building sequence should be initialized without creating a new object each time, so this is implicitly done when calling addFirstEdge(…).
Another interesting bit of this builder (as fluent interface) both methods and(..) + addFirstEdge(..) are returning the builder-object themselves, so building up multiple edges can be chained together like: addFirstEdge(“A”,”B”).and(“B”,”C”).

Summary

The fluent interface pattern is a simple yet powerful way to create an api, which offers client code only the neccessary methods and parameters and encapsulates the logic behind transparently. The example I showed made test-data builder code much more concise and better maintainable. If you’re interested more, how this litte builder api made other test cases better readable just have a look at respective test classes of open-source project dependency-analyzer.
Another lesson to learn is again: Ugly code (as my first test case was indeed) is not carved in stone, merely you are asked to improve it with the powerful refactoring concept.

Tags: Software Engineering

0 responses

    You must log in to post a comment.