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);
Related:
1 2 3 Page 1
Page 1 of 3