Saturday, December 02, 2006

Handling Rules with Functors and Spring

The thought process leading to this post began as a result of a conversation about the possibility of customizing an existing process for a new customer. The process is modelled as a series of transformations on an input object, and the customization consisted of pulling out certain parts in the series and replacing them with new ones.

I first thought of using a Rules engine such as Drools, but then I realized that it was probably overkill. All I really wanted was to pull out the conditional logic outside the code and into configuration. That way I could create a new customized process simply by cloning and changing the configuration, without (hopefully) having to touch a line of existing code. Obviously, the new components to supply the new functionality would have to be coded and inserted into the configuration, unless of course, we could reuse some of the existing components by reconfiguring it.

To model conditional logic, I decided to use the Functor classes from the Apache Commons Collections project. The configuration could be stored as structures of Functors in the Spring application context. This article describes a very small proof of concept I wrote to verify to myself that this was possible.

Basically, we can think of the process as a pipeline of multiple small processes, connected by conditionals. The object that is to be processed moves through the pipeline. The input to the pipeline will consist of an almost empty object, which gets progressively populated as it travels through the pipeline. At the output end, we get a fully populated object. The example basically tries to take the following snippet of code and move it out to Spring configuration. The DataHolder is a plain POJO with two member variables - data and result, and associated getters and setters.

1
2
3
4
5
6
7
8
  public void doPipelineWithoutFunctors() throws Exception {
    if ("foo".equals(dataHolder.getData())) {
      dataHolder.setResult("Got a foo");
    } else {
      dataHolder.setResult("Got a bar");
    }
    LOGGER.debug("Result = " + dataHolder.getResult());
  }

The first iteration using anonymous Closure and Predicate Functor objects looks like this. This is significantly more verbose than our original pipeline, but our objective is to move this logic out of the code in any case, so we are not too concerned about that.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  public void doPipelineWithFunctors() throws Exception {
    Closure[] closureList = {
      new IfClosure(
        new Predicate() {
          public boolean evaluate(Object object) {
            if (!(object instanceof DataHolder)) {
              LOGGER.debug("Bad object");
              return false;
            }
            DataHolder dh = (DataHolder) object;
            return "foo".equals(dh.getData());
          }
        },
        new Closure() {
          public void execute(Object input) {
            ((DataHolder) input).setResult("Got a foo");
          }
        },
        new Closure() {
          public void execute(Object input) {
            ((DataHolder) input).setResult("Got a bar");
          }
        }
      ),
      new Closure() {
        public void execute(Object input) {
          LOGGER.debug("Result = " + ((DataHolder) input).getResult());
        }
      }
    };
    ChainedClosure chain = new ChainedClosure(closureList);
    chain.execute(dataHolder);
  }

Based on the above, we know that the entire process can be modelled as a ChainedClosure, consisting of an IfClosure and a regular Closure object. The IfClosure uses a Predicate to determine the value of the DataHolder.data variable, and directs to one of the Closures based on the value of this variable. The second Closure simply prints out the value of the variable.

Once we extract all these values out into the Spring configuration (shown below), the our application logic can be as simple as this:

1
2
3
  public void doPipelineWithFunctorsAndSpring() {
    closure.execute(dataHolder);
  }

Of course, TANSTAAFL, and while the code is now really short, the configuration is now quite large, and arguably, as unmaintainable as the code we started out with. However, we do acheive our original goal, which is to pull all the application logic out into the configuration.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<beans ...>
  <!-- Input object -->
  <bean id="dataHolder" class="com.mycompany.functors.DataHolder" /> 
  <!-- Closure definition -->
  <bean id="pipelineClosure" class="org.apache.commons.collections.functors.ChainedClosure">     <constructor-arg>
      <list>
        <ref bean="checkDataClosure" />
        <ref bean="showResultClosure" />
      </list>
     </constructor-arg>
  </bean>

  <bean id="checkDataClosure" class="org.apache.commons.collections.functors.IfClosure">
    <constructor-arg ref="dataEqualsPredicate" />
    <constructor-arg ref="positiveResultSetterClosure" />
    <constructor-arg ref="negativeResultSetterClosure" />
  </bean>

  <bean id="dataEqualsPredicate" class="com.mycompany.functors.pipeline.DataEqualsPredicate" />

  <bean id="positiveResultSetterClosure" class="com.mycompany.functors.pipeline.ResultSetterClosure" /> 
  <bean id="negativeResultSetterClosure" class="com.mycompany.functors.pipeline.ResultSetterClosure" /> 
  <bean id="showResultClosure" class="com.mycompany.functors.pipeline.ShowResultClosure" />   

  <!-- Driver definition -->
  <bean id="pipelineDriver" class="com.mycompany.functors.PipelineDriver">
    <property name="closure" ref="pipelineClosure" />
  </bean>
</beans>

We have also had to move some of the logic out of the anonymous inner classes to three new external Functor objects, two Closures and a Predicate, which are shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class DataEqualsPredicate implements Predicate {
  public boolean evaluate(Object object) {
    if (!(object instanceof DataHolder)) {
      return false;
    }
    DataHolder dh = (DataHolder) object;
    return "foo".equals(dh.getData());
  }
}

public class ResultSetterClosure implements Closure {
  public void execute(Object input) {
    ((DataHolder) input).setResult("Got a " + ((DataHolder) input).getData());
  }
}

public class ShowResultClosure implements Closure {
  private static final Logger LOGGER = Logger.getLogger(ShowResultClosure.class);
 
  public void execute(Object input) {
    LOGGER.debug("Result = " + ((DataHolder) input).getResult());
  }
}

Obviously, this is a simple example, and to do this kind of thing for any real system, the Spring configuration is likely to be significantly more complicated. But this is true for configuration for a rule engine as well. What this approach gives us is a way to factor out our workflow rules into configuration, and all this using POJOs.

No comments:

Post a Comment

Comments are moderated to prevent spam.