Using the if-then-else framework, Part 3

Enhance the framework to support large-scale projects

The if-then-else framework is a tool for coding complex branching logic in a maintainable way. In the first two parts of this series, I described how to use the framework in a simple but typical case. The purpose of this final installment is to study the problems that arise when you place more significant demands on the framework. I will show how its performance starts to bog down as you increase the number of conditions, rules, and actions. I will then identify the performance bottlenecks, introduce a sequencing mechanism that will automate bookkeeping, and introduce an automated consistency checker that will throw an exception if inconsistent rules have been loaded. These modifications will not alter the fundamental steps for using the framework, already described in Parts 1 and 2. The end result will be a tool for handling branching logic that is industry ready and much more efficient.

TEXTBOX:

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

:END_TEXTBOX

For convenience, Listing 1 below displays the implementation of the if-then-else framework for the example used in Parts 1 and 2. I suggest you review the use of Conditions, Actions, Rules, the Updateable interface, and the Invoker subclass before diving into the rest of the discussion. (You may also wish to compare my approach to coding branching logic with the Hashed Adapter Objects design pattern, discussed in Mark Grand's book on design patterns -- see Resources for more information.)

Listing 1. The URLProcessor_good class 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;
            }
        }
    }
}

Main issues

The following issues, which I outlined at the end of Part 2, will provide a structure for this discussion:

  1. Order of evaluation. As you can see in Listing 1, the comments in the loadRules() method spell out the intended order of evaluation. I always consider conditions in the following order, whether I am loading conditions or creating a sequence of Booleans: East, West, Other, Limit, Member.

    But what happens when the number of conditions grows to 15 or 20? You can no longer expect to rely on a difficult-to-read set of notes in your documentation as a reliable means to guarantee adherence to a particular order of evaluation. Clearly, this part of the framework begs for an automated solution that can eliminate human error.

  2. Performance. In Listing 1, you will notice that one of the exceptions that is handled in the decideURL() method (which is the point of control for handling exceptions) is the NestingTooDeepException. The framework will throw this exception whenever the number of Condition instances exceeds 30. This limitation is in place to guard against unexpectedly poor performance. When the number of conditions gets too close to 30, performance becomes unacceptably slow. You can actually force the framework to falter with as few as 12 artificially created conditions (I will show how this works shortly). A review of the framework code will reveal one main reason for performance degradation: the presence of two exponential-time algorithms. Here you need faster algorithms that will improve performance under all conditions.
  3. Consistency checking. In Part 2, I pointed out how careless use of wildcards in creating Boolean sequences can easily result in a pair of inconsistent rules. Recall that a rule is a matchup between a Boolean string and a corresponding Action instance. Typically, you add a rule to the application's list of rules using the addRule(String boolStr, Action action) method of Invoker. Two such rules are inconsistent if the same Boolean string is matched with two or more different actions.

    It is easy to introduce inconsistent rules inadvertently when using wildcards. For instance, suppose you have Action instances action1 and action2. You add one rule that associates the string F*TT to action1 and later add another rule that associates the string FF*T to action2. Because the wildcard (*) indicates that both the true (T) and false (F) cases have to be considered, these rules implicitly associate the string FFTT to action1 in the first case and to action2 in the second. In other words, the use of wildcards in these two rules has introduced an inconsistency. The framework, as it operates now, will not detect the inconsistency, and will quietly select one of the rules to use. Here you need an automated consistency checker that will alert the user to any inconsistency as soon as it arises during the rule-loading process.

Order of evaluation

To reduce the risk of error in adhering to an evaluation order, you need to automate the procedure that guarantees that the same order that is used in loading conditions is also used in building up Boolean strings during the rule-loading phase. To do this, you need to move the order specification out of the comments section and into the code, and then use this piece of code to properly arrange the conditions and the Boolean strings as you load them.

One way to specify an order of evaluation within code is to associate the objects to be ordered with integer constants. You can then use the natural ordering of the integers to maintain your intended order of objects. To be concrete, I will create an interface of appropriately named constants that specifies an order of evaluation for my main example (see Listing 1):

public interface OrderOfEvaluation {
            final int EAST_CONDITION  = 0;
            final int WEST_CONDITION  = 1;
            final int OTHER_CONDITION = 2;
            final int LIMIT_CONDITION = 3;
            final int MEMBER_CONDITION= 4;
}

This simple device precisely specifies the following order for evaluating conditions: East, West, Other, Limit, and Member. As soon as my Invoker subclass implements the new interface, it can use these constants to preserve the order by passing them as arguments as Invoker loads up conditions and rules. To handle the logic involved and provide the necessary public methods, I provide wrappers, called sequencers, for the vector of conditions and table of rules.

I'll begin with the implementation of a ConditionSequencer:

public class ConditionSequencer {
    Vector conditions;
    public ConditionSequencer(Vector conditions) {
        this.conditions = conditions;
    }
    public void addCondition(Condition c, int position) throws RuleNotFoundException {
        if(conditions == null) throw new RuleNotFoundException();
        if(position >= conditions.capacity()) throw new RuleNotFoundException();
        conditions.insertElementAt(c, position);
   }
}

You instantiate ConditionSequencer by passing in the vector of conditions that you need to load. You can then add conditions one by one using the addCondition() method. This method is very much like the addElement(..) method provided by the Vector class, except that it requires as a second argument the position in Vector to store the condition. This integer argument is just the corresponding interface constant. A call to addCondition(), therefore, inserts the specified condition into the vector of conditions at the specified location. A RuleNotFoundException is thrown if you fail to initialize conditions or fail to set its capacity to a sufficiently large value. The code below shows how you would begin to rewrite the loadConditions() method in Listing 1, using this new approach:

public void loadConditions() throws IllegalExpressionException, RuleNotFoundException{
    try {  
       conditions = new Vector(numConditions);
       ConditionSequencer sequencer = new ConditionSequencer(conditions);
       sequencer.addCondition(new Condition(db.getRegion(),DataBank.EAST_REGION), 
                              EAST_CONDITION);
       sequencer.addCondition(new Condition(db.getRegion(),DataBank.WEST_REGION),
                              WEST_CONDITION);
          . . .
     }
     catch(Exception e){}
}

The technique for sequencing rules is similar but slightly more complicated. Here, the crucial step in preserving the order of evaluation lies in the arrangement of characters in each Boolean string. The proper order of the characters in TFTF requires that the T at position 0 signify that condition 0 should evaluate to true, and that the F at position 1 signify that condition number 1 should evaluate to false, and so forth. You can use the interface constants to ensure that this matchup is occurring correctly by encapsulating the requirement in a small class containing both a Boolean value and the appropriate condition name. I call this class ConditionValue; a skeleton of the class appears below:

public class ConditionValue {
    private String value;
    private int position;
    //constructor
    public ConditionValue(int pos, char c); 
}
Related:
1 2 3 4 5 Page 1
Page 1 of 5