Using the if-then-else framework, Part 2

The final steps to coding branching logic without nested ifs

In Part 1 of this article, I introduced the if-then-else framework, a single-package framework that makes it relatively easy to code branching logic without nested ifs, in a maintainable form. The discussion in both parts of the article focuses on how to use the framework and revolves around the task of rewriting a typical piece of nested-if code having three levels of nesting (see Listing 1 below, reproduced from Part 1 for convenience).

Listing 1. Sample nested-if code

void decideUrl() {
     DataBank.Region region = db.getRegion();
     double limit = db.getLimit();
     String id = db.getUserId();
     if(region.equals(DataBank.EAST_REGION)) {
         if(limit > db.LIMIT_THRESHOLD) {
             setUrl(EAST_PRIVILEGED);
         }
         else {
             setUrl(EAST_NOT_PRIVILEGED);
         }
     }
     else if(region.equals(DataBank.WEST_REGION)) {
         if(isMemberWestAlliance(id)) {
             if(limit > db.LIMIT_THRESHOLD) {
                 setUrl(WEST_MEMBER_PRIVILEGED);
             }
             else {
                 setUrl(WEST_MEMBER_NOT_PRIVILEGED);
             }
         }
         else {
             if(limit > db.LIMIT_THRESHOLD) {
                 setUrl(WEST_NONMEMBER_PRIVILEGED);
             }
             else {
                 setUrl(WEST_NONMEMBER_NOT_PRIVILEGED);
             }
         }
     }
     else {
         setUrl(OTHER_REGION);
     }
 }

TEXTBOX:

TEXTBOX_HEAD: Using the if-then-else framework: Read the whole series!

:END_TEXTBOX

Recall that for the sake of having runnable code, I have wrapped this version of the decideURL() method in a class URLProcessor_bad in the downloadable sample code. The implementation of the if-then-else framework in this example will result in a rewrite of decideURL(). (This new version of the method appears in the class URLProcessor_good; see Resources.)

In Part 1, I outlined the main framework classes with which you will work and the four steps required to implement the framework, listed again here:

  1. Determine the conditions. Determine the individual conditions involved in the logic at hand, and implement each as an instance of Condition (or possibly of a Condition subclass).
  2. Determine the actions. Determine the possible actions that need to be executed and implement each as an instance of Action (or possibly of an Action subclass).
  3. Implement the Updateable interface. Decide which object(s) should receive the actions implemented in Step 2 and make sure that it (they) implements the Updateable interface (which means you need to implement the doUpdate() method).
  4. Subclass the Invoker class. Create a concrete invoker subclass of Invoker and implement the loadConditions() and loadRules() methods.

By the end of Part 1, I completed the analysis of the conditions involved in rewriting the decideURL() method. Here is a quick review:

The code in Listing 1 involves three main conditions that must be evaluated for the main action — setting the value of a URL — to be fired off: a location condition, a condition on the value of a limit, and a condition concerning membership to “West Alliance.” Since the possible values of location cannot be reduced to a single Boolean-valued condition, I broke down the location condition into three finer-grained conditions: whether or not the specified region is EAST_REGION, whether or not it is WEST_REGION, and whether or not it is neither. The analysis of conditions resulted in the creation of five objects, which completed the work for Step 1:

new Condition(db.LIMIT_THRESHOLD, Condition.LESS_EQUAL, db.getLimit());
new Condition(db.getRegion(), DataBank.EAST_REGION);
new Condition(db.getRegion(), DataBank.WEST_REGION);
new OtherCondition(other); //other is a Hashtable parameter
new MemberCondition(mem);  //mem is a Hashtable parameter

As mentioned before, nailing down the conditions is the hard part in this exercise. I’ll now show you how to complete the remaining steps.

Step 2: Determine the actions

Recall that I have designed the framework so that each action to be fired in response to evaluating a sequence of conditions is encapsulated as an instance of the class Action (or of an Action subclass). Action constructors all require an instance of the Updateable interface to be passed in — this instance will be the target of the Action instance’s execute() method. The Action class has the following two constructors:

Action(Updateable ud, Object attribute);
Action(Updateable ud, Hashtable attributes);

The first constructor represents the default behavior and is the easiest to use. You should use it when the action to be executed is simply setting the value of an attribute (in the Updateable object). When you use this constructor, you can use the Action‘s default execute() method without change; the attribute in the Updateable object will automatically be set for you when the action is fired. Let’s take a moment to examine the implementation of the first Action constructor, and the implementation of the execute() method:

public Action(Updateable ud, Object attribute) {
    this.ud = ud;
    setAttribute(attribute);
}
public void execute() {
   Hashtable ht = new Hashtable();
   ht.put(MAIN_KEY, attribute);
   getUd().doUpdate(ht);
}

In the code, the constructor simply caches the Updateable object and the attribute to be set. Then, in the execute() method, this attribute is placed in a Hashtable, keyed on the constant MAIN_KEY (a constant in Action ). The method then sends a message to the Updateable object to update itself, using the data packed in the Hashtable.

More complex actions may require you to use the second Action constructor. In that case, you will have to subclass Action and override its execute() method. When you take this course, you use the Hashtable argument in the constructor to store any information that Action may need in order to execute. In my experience using this framework, even fairly complex actions can be handled using the first constructor; setting a single attribute in a tiny inner class can trigger other processes that start up when the value of the newly set attribute is read.

For this example, if you review the code in Listing 1, you will see, in each case, the action to be executed is simply to set a URL. Any of seven different URLs may be set, so you can expect to have seven instances of Action, each using the first of the constructors listed above.

Here is an example of how to construct one such instance of Action — it sets the URL to be EAST_PRIVILEGED. You would construct the other six instances, corresponding to the other six URLs, in an analogous fashion.

new Action(ud,EAST_PRIVILEGED));

The argument ud in this line of code is an instance of Updateable. In Step 3 below, I’ll discuss ways to select an appropriate object to implement this interface and how to implement its doUpdate() method.

Step 3: Implement the Updateable interface

For an instance of Action to execute, it must have access to the object that is to be modified. However, it would be poor object-oriented design to allow an Action instance full access to such objects, which could literally be any objects that are not read-only. Indeed, Action instances should know very little about the rest of the application. Java interfaces let you provide exactly the access you want an Action instance to have: the Action instance knows only how to send the message “update yourself” (via the doUpdate(..) method) to such objects, passing in whatever data might be needed for this process.

Because of the safety afforded by using the Updateable interface, you can select any convenient object to receive the result of any Action instance you create. In each case, there are two requirements:

  1. The class of the object you select must implement the Updateable interface
  2. The class of the object you select must implement the doUpdate() method

In the example here, I have chosen to use the main class URLProcessor_good to receive the URL-setting actions because, in the sample code, this class bears the responsibility for processing URLs. I have added the clause implements Updateable to the class declaration of URLProcessor_good and have implemented doUpdate() as follows:

public void doUpdate(Hashtable ht) {
    Object ob = ht.get(Action.MAIN_KEY);
    if(ob instanceof String) {
       setUrl((String)ob);
    }
}

In this implementation, the method unpacks the Hashtable argument using the Action constant MAIN_KEY as the key. The associated value is expected to be a string that names a URL; so, after doing a type-safety check with the instanceof operator, the URLProcessor method setUrl(..) is invoked, which converts its input string to a URL instance, and assigns the protected URLProcessor instance variable url to this value.

Step 4: Subclass the Invoker class

The Invoker class is responsible for assembling the pieces obtained in previous steps and providing this data to the framework’s engine, which can then evaluate the Conditions and fire off the appropriate Actions. Most of the work involved is done for you in the framework code — all you need to do is implement two abstract methods in Invoker: loadConditions() and loadRules(). These must be implemented in an appropriately defined subclass. In the present example, I have called this subclass ConcreteInvoker.

To implement these methods properly, you must decide on an order for evaluating conditions — I will call this order the order of evaluation. The particular order of evaluation you choose is irrelevant, but the order you do choose must be used consistently in both loadConditions() and loadRules(). To make this point more concrete, let’s look at the order of evaluation I have chosen. I have decided (completely arbitrarily) that the five conditions will be evaluated in the following order: “East Condition,” “West Condition,” “Other Condition,” “Limit Condition,” and “Member Condition.”

This order will dictate the order in which the loadConditions() method will load the Condition instances into a Vector, and it will also dictate the configuration of the rules to be loaded by loadRules().

You’ll begin with the implementation of loadConditions(). This method must load the Invoker instance variable conditions (a Vector) with all five conditions in the prescribed order. Here is the code:

public void loadConditions()  {
    conditions = new Vector(5);
    //East condition
    conditions.addElement(new Condition(db.getRegion(),DataBank.EAST_REGION));
    //West condition
    conditions.addElement(new Condition(db.getRegion(),DataBank.WEST_REGION));
        //Other condition
   Hashtable other = new Hashtable();
   other.put(OtherCondition.KEY,db.getRegion());
   conditions.addElement(new OtherCondition(other));
   //Limit condition
   conditions.addElement(new Condition(db.LIMIT_THRESHOLD,
                                       Condition.LESS,
                                       db.getLimit() ));
   //Member condition
   Hashtable mem = new Hashtable();
   mem.put(MemberCondition.TABLE, db.getWestMembers());
   mem.put(MemberCondition.KEY, db.getUserId());
   conditions.addElement(new MemberCondition(mem));
}

Notice that this code simply carries out the instantiation of Conditions in the manner described in Step 1 (see Part 1 of this series), and then loads them one-by-one into the conditions Vector, in accord with my specified order of evaluation.

Implementing the loadRules() method requires a bit more care. This method encapsulates all the business logic that you are introducing into the if-then-else framework. In the body of this method, you must associate to each sequence of Booleans the appropriate action. Each sequence of five Booleans in the present example corresponds to true/false values for each of the five conditions you have isolated, arranged in accord with the specified order of evaluation. As discussed in Part 1, you represent a sequence of Booleans as a string of Ts and Fs (where T stands for “true” and F for “false”). Thus, for example, the string “FTFFF” encodes a sequence of Booleans that has the following meaning (notice the importance of the order of evaluation here):

  • “East Condition” evaluates to false
  • “West Condition” evaluates to true
  • “Other Condition” evaluates to false
  • “Limit Condition” evaluates to false
  • “Member Condition” evaluates to false

If you review the code in Listing 1, you will see that this particular combination of conditions corresponds to the action of setting the URL to WEST_NONMEMBER_NOT_PRIVILEGED. Therefore, you will associate “FTFFF” with the Action constructed with the string WEST_NONMEMBER_NOT_PRIVILEGED.

The mechanism for associating a sequence of Booleans (represented as a string of Ts and Fs) with a particular Action instance lies in the framework class Rules. This class provides a method addRule(String boolStr, Action action) that accepts a string (like “FTFFF”) and an instance of Action as arguments. Therefore, to implement loadRules(), you must assign an instance of Rules to the rules instance variable and then repeatedly apply the addRule(..) method. The rule-loading code corresponding to the string “FTFFF” would look like this:

Action westNonmemNonpriv = new Action(ud,WEST_NONMEMBER_NOT_PRIVILEGED);
rules.addRule("FTFFF", westNonmemNonpriv);

For this particular rule, the values for all five conditions were specified (each of the five conditions is associated with either a T or an F in the string “FTFFF”). Luckily, in your rewrite of decideURL(), it is not always necessary to specify Boolean values for all five conditions to uniquely determine the Action instance that should be fired (if it were necessary, you would need to specify a rule for each of the 32 possible sequences of Ts and Fs). In fact, since there are just seven distinct Actions to be fired, you need only specify seven distinct rules. For instance, the rule that sets the URL to OTHER_REGION requires specification of only the first three conditions: “East” must be false, “West” must be false, and “Other” must be true. Once these three conditions have evaluated to these values, the URL will have to be set to OTHER_REGION, regardless of the values of the other two conditions.

To accommodate this idea that you “don’t care” about the values of certain conditions, I have allowed the commonly used wildcard symbol “*” as one of the symbols that can occur in the string argument boolStr. For instance, the Boolean string that should be associated with the OTHER_REGION Action would be “FFT**” since the last two conditions (“Limit Condition” and “Member Condition”) do not play a role in determining the corresponding Action in this case. The corresponding rule-loading code would look like this:

Action otherReg = new Action(ud,OTHER_REGION);
rules.addRule("FFT**", otherReg);

The Rules class is equipped to handle wildcard symbols properly. I will discuss the techniques I used for parsing such strings in the third part of this series, but I need to mention here how the framework processes these strings, to steer you away from possibly misusing wildcards.

It is indeed possible to use the wildcard symbol unwisely as you add rules. To see what can go wrong, consider the reasoning involved as you form a Boolean string to match with the Action westNonmemNonpriv. (This was the Action associated with the string FTFFF that I used in the first example of loading rules.) You might reason as follows:

Once I know that “West Condition” evaluates to true and both “Limit Condition” and “Member Condition” evaluate to false, the only possible action that could be fired is the action that sets the URL to WEST_NONMEMBER_NOT_PRIVILEGED. In other words, the other two location conditions, “East Condition” and “Other Condition” play no role in determining which action is fired. Therefore, I can use the Boolean string *T*FF as the string to match with this action.

At first, the reasoning seems sound, and, as far as this example goes, the string *T*FF would work just as well as the string I suggested, FTFFF. The problem with this implementation has to do with the way in which the framework code handles wildcards. The framework views an occurrence of a wildcard as a request to consider both the true case and the false case. For instance, the string FTFF* is processed as two strings: FTFFT and FTFFF (the “*” is replaced by T in the first case and F in the second). In the case of the string *T*FF, the framework would process all possible truth values for the first and third positions. The effect would be to add four strings, all associated with the single Action westNonmemNonpriv:

  • TTTFF
  • TTFFF
  • FTTFF
  • FTFFF

To be specific, notice that the framework would have to process the situation in which all three location-conditions end up being true (given by the string TTTFF). According to the sample code so far, such a combination could never occur (a user can only be in one region) and appears harmless. But suppose the sample were part of a real application, and a business decision is made later that permits a user to be considered part of several regions. And suppose the corresponding URL for users who are part of all regions ends up being something different from the seven URLs you have specified so far, say, NEW_URL. Then the programmer would have to add the following rule as part of the maintenance task:

Action newAction = new Action(ud,NEW_URL);
rules.addRule("TTTFF", newAction);

And that may be all the programmer remembers to do during maintenance. It’s very likely the programmer will fail to notice that the string *T*FF has been matched with westNonmemNonpriv and will therefore fail to make the appropriate adjustments. So what happens when the framework attempts to process these two strings, *T*FF and FTFFF? The framework will end up adding a rule for FTFFF twice, one time associating with it the Action westNonmemNonpriv and the other time associating the Action newAction. Only one of these rules, the last one to be added, will survive.

This kind of scenario can arise only through careless use of the wildcard symbol. Fortunately, you can safely avoid it by adhering to a simple implementation rule, which I call the Wildcard Rule.

The Wildcard Rule. In forming Boolean strings for use in the addRule(..) method, use a wildcard symbol for a particular Condition only as a convenient substitute for writing out two strings, one in which the Condition is true, the other in which the Condition is false, and both always resulting in firing the same Action.

In the scenario above, you could never have used the string *T*FF in place of FTFFF if you’d followed the Wildcard Rule, because in this case the rule would imply that you wish the framework to treat all four strings TTTFF, TTFFF, FTTFF, FTFFF the same, and have them fire off the same Action. (There would be no reason to commit yourself to a particular action for any of the first three of these strings.) On the other hand, the Wildcard Rule suggests that you should use wildcards as you did in the second example above, where the string was FFT**, because in this case you do wish to view each of the strings FFTTT, FFTTF, FFTFT, and FFTFF as the same, all firing off the action that sets the URL to OTHER_REGION.

So far, I have described two of the rules you need to add in implementing loadRules(). The same principles apply as you code the remaining five rules. The implementation of the entire loadRules() method follows (with exception-handling omitted):

public void loadRules(){
    rules = new Rules();
    //             Conditions:               Actions:
    //        East|West|Other|Lim|Member
    rules.addRule("TFFT*",                      new Action(ud,EAST_PRIVILEGED));
    rules.addRule("TFFF*",              new Action(ud,EAST_NOT_PRIVILEGED));
    rules.addRule("FTFTT",                      new Action(ud,WEST_MEMBER_PRIVILEGED));
    rules.addRule("FTFTF",                      new Action(ud,WEST_NONMEMBER_PRIVILEGED));
    rules.addRule("FTFFT",              new Action(ud,WEST_MEMBER_NOT_PRIVILEGED));
    rules.addRule("FTFFF",              new Action(ud,WEST_NONMEMBER_NOT_PRIVILEGED));
    rules.addRule("FFT**",              new Action(ud,OTHER_REGION));
}

Notice that I have reproduced the specified order of evaluation in the inline comments. This is a practical point of some importance. Of all the code that you need to implement the framework, the loadRules() method is where you are most likely to make an inadvertent coding error. You can remove much of the risk simply by reproducing the order of evaluation in the comments so that it is visible as you type in the necessary Ts and Fs. Doing this will also make the job of maintaining this section of code significantly easier.

After you have coded the loadConditions() and loadRules() methods for your Invoker subclass (in this example, ConcreteInvoker), your code is basically ready to go. What remains to be done is to handle exceptions and to do appropriate packaging of these new classes.

Handling exceptions

The if-then-else framework includes four custom-built subclasses of Exception: DataNotFoundException, IllegalExpressionException, NestingTooDeepException, and RuleNotFoundException.

You should throw a DataNotFoundException when data that you expect to find is not present. The framework itself never throws this exception, but does declare that the evaluate() method of Condition can throw it. For the default implementation of evaluate(), the exception is not thrown, but when you use the Condition(Hashtable) version of the constructor, and therefore write your own evaluate() method, you should test whether the variable data in Condition is null, and, if not, whether the expected values in this Hashtable are really there; if not, you should throw a DataNotFoundException. Following these guidelines, your code for the evaluate() method of OtherCondition should look like this:

public Boolean evaluate() throws DataNotFoundException {
    Object ob = null;
    if(getData() == null || (ob = getData().get(KEY)) == null) {
        throw new DataNotFoundException();
    }
    if(!(ob instanceof DataBank.Region)) {
        return Boolean.FALSE;
    }
    DataBank.Region region = (DataBank.Region)ob;
    boolean result = !region.equals(DataBank.WEST_REGION) &&
                     !region.equals(DataBank.EAST_REGION);
    return new Boolean(result);
}

I created the IllegalExpressionException for one purpose: In using one version of the Condition constructor (like Condition(double d1, int oper, double d2), which I examined in Part 1), you must pass in an integer code for an operator (like Condition.LESS or Condition.EQUAL). If you pass in an unrecognized integer value, the constructor throws an IllegalExpressionException. So, unlike the DataNotFoundException, the IllegalExpressionException is thrown only by the framework classes. As the user of the framework, you will catch, but never throw, an IllegalExpressionException.

The NestingTooDeepException is thrown when you attempt to use too many conditions. The maximum number of conditions is stored in the constant MAX_LEVELS_OF_NESTING in BooleanTupleConstants. In the current version of this framework, you can create at most 30 instances of Condition. The reason for this limitation has to do with the implementation of rules and sequences of Booleans. In Part 3, I will discuss a way to modify the framework so that there is no such limit and assess the price you have to pay for this modification.

The NestingTooDeepException is actually thrown by the method getTuple(String tf) in the framework class BooleanTupleFactory. This method checks to see how long the passed in string tf is and if its length exceeds the value of MAX_LEVELS_OF_NESTING, an instance of this exception is thrown. Once again, only the framework classes should throw this exception.

Finally, the framework throws a RuleNotFoundException whenever a rule lookup fails. When you ask your Invoker subclass to execute, it instantiates the IfThenElse class, which in turn attempts to execute an Action, based on a sequence of Conditions that it has received. After evaluating these Conditions and placing the resulting sequence of Booleans in a BooleanTuple, it attempts to read off, from the ruleTable, the Action that is associated with this BooleanTuple. If this BooleanTuple is not a key for the ruleTable, or if no Action instance is found as a matching value, the framework throws a RuleNotFoundException.

How and where do you handle these exceptions in your code? The sample should provide a useful guide, even if you decide to do more with exceptions than I have here. In the example, I have centralized all exception handling to a single point of control: All exceptions are thrown and rethrown until they arrive inside the original calling method. In the example, the original calling method, which is the method in which your implementation of Invoker is instantiated, is decideURL(). As the code below shows, the method tries to instantiate ConcreteInvoker (this attempt could result in a NestingTooDeepException) and then run its execute() method (which could result in an IllegalExpressionException, RuleNotFoundException, or a DataNotFoundException). Each possible exception has its own catch clause that can be implemented in its own way. Each type of exception has its own default message, which you can retrieve by calling the Exception method getMessage(). As usual, you can set your own messages in these exceptions by invoking the constructor that accepts a string argument (except in the case of RuleNotFoundException, where the string argument is expected to be a string representation of the BooleanTuple that failed to produce a corresponding Action during a rule lookup).

void decideUrl() {
    try {
        ConcreteInvoker ci = new ConcreteInvoker((Updateable)this);
        ci.execute();
    }
    catch(NestingTooDeepException ntde)   {//handle}
    catch(IllegalExpressionException iee) {//handle}
    catch(RuleNotFoundException rnfe)     {//handle}
    catch(DataNotFoundException dnfe)     {//handle}
}

Packaging classes

As you can see in this example, implementing the if-then-else framework involves creating subclasses of the framework classes Invoker, Condition, and Action. Specifically, in this example, you have created the following three subclasses:

  • ConcreteInvoker (subclass of Invoker)
  • OtherCondition (subclass of Condition)
  • MemberCondition (subclass of Condition)

Because the actions used here were very simple, there was no need for you to create subclasses of Action, although in other applications of the framework, you would probably have to create a few.

How should you package these subclasses? When you use the framework in isolated areas of a project, a sensible packaging strategy is to embed all your subclasses as inner classes of the class that contains the calling method. Doing so makes the code more readable and maintainable, since all the logic involved can be found in one place. In this case, the outer class is URLProcessor_good, in which I have packaged the three subclasses as inner classes. You can see the code in Listing 2 below.

Listing 2. The class URLProcessor_good with user-defined inner classes

class URLProcessor_good extends URLProcessor implements logic.Updateable {
    static final String URL_STR = "urlString";
    URLProcessor_good(){
        super();
    }
    void decideUrl() {
        try {
            Invoker inv = new ConcreteInvoker((Updateable)this);
            inv.execute();
        }
        catch(NestingTooDeepException ntde) {}
        catch(IllegalExpressionException iee) {}
        catch(RuleNotFoundException rnfe) {}
        catch(DataNotFoundException dnfe) {}
    }
    /** Updateable interface implementation */
    public void doUpdate(Hashtable ht) {
        if(ht == null) return;
        Object ob = ht.get(Action.MAIN_KEY);
        if(ob != null && ob instanceof String) {
            setUrl((String)ob);
        }
    }
    static class OtherCondition extends Condition {
        public static final String KEY = "key";
        public OtherCondition(Hashtable ht) {
            super(ht);
        }
        public Boolean evaluate() throws DataNotFoundException {
            Object ob = null;
            if(getData() == null || (ob = getData().get(KEY)) == null) {
                throw new DataNotFoundException();
            }
            if(!(ob instanceof DataBank.Region)) {
                return Boolean.FALSE;
            }
            DataBank.Region region = (DataBank.Region)ob;
            Boolean result = !region.equals(DataBank.WEST_REGION) &&
                             !region.equals(DataBank.EAST_REGION);
            return new Boolean(result);
        }
    }
    static class MemberCondition extends Condition {
        public static final String KEY = "id";
        public static final String TABLE = "table";
        public MemberCondition(Hashtable ht) {
            super(ht);
        }
        public Boolean evaluate() throws DataNotFoundException {
            Object id = null, table = null;
            if(getData() == null || ((id = getData().get(KEY)) == null) ||
              ((table = getData().get(TABLE))==null) ||
              !(table instanceof Hashtable)) {
                  throw new DataNotFoundException();
            }
            Hashtable members = (Hashtable)table;
            return new Boolean(members.containsKey(id));
        }
    }
    public class ConcreteInvoker extends Invoker {
        public ConcreteInvoker(Updateable ud) throws NestingTooDeepException {
            super(ud);
        }
        public void loadRules() throws NestingTooDeepException {
            rules = new Rules();
            try {
                //             Conditions:                 Actions:
                //        East|West|Other|Lim|Member
                rules.addRule("TFFT*",          new Action(ud,EAST_PRIVILEGED));
                rules.addRule("TFFF*",          new Action(ud,EAST_NOT_PRIVILEGED));
                rules.addRule("FTFTT",          new Action(ud,WEST_MEMBER_PRIVILEGED));
                rules.addRule("FTFTF",          new Action(ud,WEST_NONMEMBER_PRIVILEGED));
                rules.addRule("FTFFT",          new Action(ud,WEST_MEMBER_NOT_PRIVILEGED));
                rules.addRule("FTFFF",          new Action(ud,WEST_NONMEMBER_NOT_PRIVILEGED));
                rules.addRule("FFT**",          new Action(ud,OTHER_REGION));
            }
            catch(NestingTooDeepException e) {
                throw e;
            }
        }
        public void loadConditions() throws IllegalExpressionException {
            try {
                conditions = new Vector(5);
                conditions.addElement(new Condition(db.getRegion(),DataBank.EAST_REGION));
                conditions.addElement(new Condition(db.getRegion(),DataBank.WEST_REGION));
                Hashtable other = new Hashtable();
                other.put(OtherCondition.KEY,db.getRegion());
                conditions.addElement(new OtherCondition(other));
                conditions.addElement(new Condition(db.LIMIT_THRESHOLD,
                                                    Condition.LESS,
                                                    db.getLimit() ));
                Hashtable mem = new Hashtable();
                mem.put(MemberCondition.TABLE, db.getWestMembers());
                mem.put(MemberCondition.KEY, db.getUserId());
                conditions.addElement(new MemberCondition(mem));
            }
            catch(IllegalExpressionException e) {
                throw e;
            }
        }
    }
}

If you use the if-then-else framework more ambitiously, packaging subclasses as inner classes doesn’t make as much sense. Scaling will require a proper design of a separate rules package so that conditions and actions can be reused and interrelated in appropriate ways. In this situation, you should view the rules package as a kind of “logic engine” that receives logical setup information (conditions, actions, and rules) and some specific user input (permitting evaluation of conditions to produce a string of booleans), and which, in response, fires off an appropriate action.

I should point out, however, that the framework I have presented here is not itself a rules engine and has certain limitations to its scalability. I will discuss these limitations in Part 3 of this series. When you are faced with implementing a large volume of branching logic (which is common in rule-intensive industries like insurance), it’s wise to organize data into database tables so that the process of evaluating a sequence of conditions amounts to performing an appropriate SQL query. In this context, you will find (as I have) that certain pieces of complex logic still need to be implemented and the if-then-else framework usually provides the right solution in such cases.

Maintaining code based on the if-then-else framework

Using the if-then-else framework clearly requires more work than simply writing nested-if code. One of the rewards for this effort is greater ease of maintenance, and consequently, fewer bugs. To give you a more concrete idea of the advantages, I’ll consider next most of the typical maintenance activities for nested-if code and show how you can perform those activities more safely using framework-based code. Here are four of the most common maintenance activities for code that implements branching logic:

  1. Change the logic within a condition. In nested-if code, a condition may appear several times in different “if” blocks. You can see this situation in Listing 1: the “limit” condition appears in both the first and second block. This means that whenever any change to such a condition is required, you have to be sure to make the change in each place the condition occurs. Errors arise very easily when performing this type of maintenance. When you implement conditions as instances of Condition, however, maintenance becomes easier. The change can be made in just one place (namely, within the relevant Condition), and unlike in nested-if code, the modification isn’t cluttered with other unrelated issues, such as the details of other conditions and particular actions.
  2. Change the action that results from a particular sequence of Booleans. The maintenance challenge in this case, when you are dealing with nested-if code, is that you have to read and understand a lot of complex code to locate the proper lines to modify. In Listing 1, for instance, if you wish to change the URL that is set when a user belongs to the WEST_REGION, has a small limit, and is a member of the “West Alliance,” you must weave in and out of “if” blocks to locate the correct line to modify. You might well make a mistake in your attempt to locate this line. On the other hand, when you use the framework approach, you specify actions when you loadRules() by matching a sequence of Ts and Fs with an instance of Action. You can still make an error in locating the correct sequence of Ts and Fs, but this is a much easier task to carry out.
  3. Add a new action based on new combinations of already existing conditions.

    You may need to add granularity to your existing nested-if code — conditions that “didn’t matter” before may become important later on. In Listing 1, for instance, if a user is neither in

    EAST_REGION

    nor

    WEST_REGION

    , the resulting action will be the same regardless of his limit. You may wish to add further discrimination to this code and require that two new URLs be set: one if the limit exceeds

    LIMIT_THRESHOLD

    and the other if it does not. In this case, the change is easy to make because this particular block of code (the third block in Listing 1) contains only one line. However, this same kind of change might be required within a more complicated block and might have to be inserted in several different places within the same block, or in different blocks. At this level of difficulty, errors become very likely.

    Let’s look at how to make such changes in framework-based code. Starting again with the simple case of adding a “limit condition” to the “other” location condition (which corresponds to adding a limit condition to the third block in Listing 1), your maintenance task is to replace one rule in your rules table with two. You would remove the rule that mapped FFT** to its corresponding Action, and add two new rules: one for FFTT* and one for FFTF*. Notice that the “*” in the fourth position in FFT** has been replaced in one case by a T and in the other by an F. This solution is attractive because it never gets more difficult than this, even when dozens of conditions are involved. You always proceed by first removing a rule in which one of the Boolean strings has a “*”, and replace it with two rules, where “*” is replaced by T in one case and F in the other. This is starting to sound elegant!

  4. Add a new condition and, as a result, new actions as well. Adding a new condition is typically difficult to do in nested-if code because you have to be sure to insert it in all the “if” blocks in which it should occur. This activity entails the same risks I discussed earlier in the first maintenance activity. In the framework scenario, you can always add a condition with the following procedure: First, make the (convenient) decision that the new condition will be the last condition to be loaded in your order of evaluation. (Remember you are free to decide on any order of evaluation, as long as you use it consistently.) Then modify all Boolean strings by adding one symbol to the end. For some sequences of Booleans, the new condition “won’t matter,” and you will be able to simply add a “*” to the end and leave the corresponding actions unchanged. For strings that do need to change, you will have to replace one rule (a Boolean string matched with an Action) with two new rules (two Boolean strings matched with the same Action) — one new rule must be created for the T case and another for the F case.

I will illustrate again with the sample code. Suppose you wish to add an “East Membership” condition, applicable only to users in the EAST_REGION. You would have to replace each Boolean string that begins “TFF..” with two slightly longer strings, one that ends with T and the other that ends with F, and then you would have to add the appropriate new Action instances. All other Boolean strings would be modified by appending the “*” symbol, leaving the corresponding Actions unchanged. Here is a short catalogue of the changes involved (I took the list of Boolean strings from the snippet of code for the loadRules() method, given earlier, and show below how they would be transformed):

   TFFT* --> TFFT*T       FTFTF --> FTFTF*
             TFFT*F       FTFFT --> FTFFT*
   TFFF* --> TFFF*T       FTFFF --> FTFFF*
             TFFF*F       FFT** --> FFT***
   FTFTT --> FTFTT*

What’s next?

In the first two parts of this article, my aim has been to make the if-then-else framework accessible in a practical way. After working through the four steps of implementation in some detail for the URLProcessor example, you should be able to import the logic package (see Resources) into your own projects to implement complex branching logic. In Part 3 of the article, the focus will shift to a more detailed study of the framework’s design. This study will help to answer a number of important questions that will inevitably arise as you use the framework: Why is there a limit of just 30 conditions? How does the framework perform in comparison with ordinary nested-if code, especially as the number of conditions increases? If performance is an issue, where are the bottlenecks and how can they be fixed?

In Part 3, I will discuss performance issues, identify bottlenecks, and offer some significant optimizations that will improve the framework’s scaling potential tremendously. I will also offer solutions to two risk areas, one having to do with the order of evaluation you must specify before implementing the loadConditions() and loadRules() methods in the Invoker subclass and the other having to do with correct application of the Wildcard Rule. Part 3 will show how to fine-tune the framework to handle scaling issues and manage risks so that it can serve as a versatile and robust tool in the workplace. A new expanded version of the framework code, containing all the refinements discussed, will accompany the final part of the article.

Paul Corazza is a software professional who
has applied his PhD training in mathematical logic to a broad range
of pure and applied fields in the past 12 years. In the last four
years, he has concentrated on the development of business rules
frameworks, joining design patterns from the object-oriented world
with tools for analysis of syntax from the world of logic. Paul is
president and lead consultant of Corazza Software Solutions, a Java
consulting firm that provides software engineering and training
services.

Source: www.infoworld.com