Using the if-then-else framework, Part 1

Code maintainable branching logic with the if-then-else framework

No one should have to read or maintain code shown in Listing 1 below. Code that includes nested ifs, as in this example, can be a maintenance nightmare. It is usually difficult to read and understand, and even more difficult to modify, which makes the code highly error-prone.

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 But what are you supposed to do when you need to write code that has branching logic? Often, you can sidestep the issue by making sure that lookup values are stored in a database. If the database solution is not an option, you can usually solve the problem by mimicking the database solution with Java Hashtables. But if you have to repeatedly implement this solution, with slightly different conditions and events, you will discover as I have, a burning need for a more general solution.

This article is about a little framework I have developed for writing code with branching logic without nested ifs. The framework resides in a single package and has a simple API that you must use. Using this framework, you can rewrite the code in Listing 1 in just a few lines (shown below), and encapsulate the conditions and consequences in a table that is easy to maintain.

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

My discussion of this framework will span three issues of JavaWorld. The first two parts of the series are devoted to an explanation of how to use the framework. As I develop my ideas, I will show you how to code the branching logic of Listing 1 without nested ifs. After reading these first two parts, you should easily be able use the framework in your own work -- simply import the logic package (see Resources) and follow the steps described in the article. In the third part of the series, I will explain in greater detail the design and implementation of the framework, and will discuss refinements that you can introduce if desired. The seed ideas for this framework are among the many profound design secrets I learned from Ali Arsanjani during informal discussions in 1998 (see Resources).

You can download all of the code that is used in these articles from Resources. You can see the logic of decideURL() (Listing 1) in action by loading the HTML page IteGUI.html; this file loads the IteGUI applet, which provides a convenient visual representation of the method's behavior. By running this applet online, you can connect to the URLs that are referred to in the code. You can also run the code as either an application or applet locally, but the URL will not be accessible in that case. See the Resources section for more details.

In order to have a runnable application with which to work, I wrapped the decideURL() method from Listing 1 in a class called URLProcessor_bad. As I rewrite this method, I will create an alternative class named URLProcessor_good that will contain a revised version of decideURL(). Both of these classes inherit from the abstract class URLProcessor. These classes will be the focal point of my discussion. I will also make use of the following two auxiliary classes that provide support for the main classes: DataBank provides access to all relevant data, and IteGUI provides the code for a simple user interface that can be used for entering data and displaying results. Figure 1 shows the relationships among these classes.

Figure 1. Static relationships among main user classes

Analysis of if-then-else logic

Before examining the components used in the framework, I'll give a quick analysis of the code structure in Listing 1, which will help to motivate the framework's design. The result of executing the code in Listing 1 is that a value for the URL field is set. The actual value is determined by evaluating several conditionals, following a branch whose nodes always evaluate to true. The logical components of this behavior are conditions and actions. If a sequence of conditions evaluates to true, the appropriate action (in this case, setting the value of a field) is performed. For instance, one scenario that could occur in Listing 1 is the following: the user's region is "East" and the limit happens to be less than or equal to LIMIT_THRESHOLD (a constant in DataBank). With these conditions satisfied, the code sets the URL to be EAST_NOT_PRIVILEGED (a constant in URLProcessor).

Elements of the if-then-else framework

This short analysis suggests two main data types or classes that you can use in this framework: Condition and Action. A Condition should encapsulate the requirements of any condition that might occur in nested-if logic, and an Action should encapsulate the ingredients for carrying out virtually any action. To this end, I have equipped Condition with the method boolean evaluate() and Action with the method void execute(). For these methods to execute correctly, the framework user must initialize them with adequate information. I have provided a number of constructors in Condition to accommodate the most common scenarios for evaluating a condition, and one more-general constructor to handle all other cases. I'll cover the details in a moment. In the case of Action, for the sake of generality I have required that an updateable object be passed in to its constructor. The execute() method of Action then performs appropriate updates on this updateable object. In order to provide generic access to objects that need to be updated, I've included a Java interface named Updateable in the framework package; the object that should be updated by an Action must implement this interface. The interface consists of a single method doUpdate():

public class Condition {
    Hashtable data;
    public Condition(double d1, String operator, double d2) {
        //set values
    }
        . . .
    //other constructors
        . . .
    public boolean evaluate() {
        //default implementation
    }
}
public class Action {
    public Action(Updateable ud, Object attribute) {
        //set values
    }
    public void execute() {
        //default implementation
    }
}
public interface Updateable {
    public void doUpdate();
}

The final ingredient you will need is a class that will manage loading up the appropriate instances of Condition and matching them with appropriate instances of Action. I named this class Invoker, which has two abstract methods -- loadConditions() and loadRules() -- that must be implemented in a user-defined subclass of Invoker. In the example below, I named this user-defined subclass ConcreteInvoker.

abstract public class Invoker {
    protected Updateable ud;
        protected Rules rules;
        protected Vector conditions;
        protected Vector actions;
    public Invoker(Updateable u) {
            this.ud = u;
            init();
    }
    public void execute() {
        loadConditions();
            new IfThenElse(conditions, rules);
        }
    private void init() {
        loadRules();
    }
    abstract public void loadConditions();
    abstract public void loadRules();
}
public class ConcreteInvoker extends Invoker {
    public ConcreteInvoker(Updateable u) {
        super(u);
    }
    public void loadConditions() {
        //implement
    }
    public void loadRules() {
        //implement
    }
}

I can now describe the flow of execution. To start the process, a controlling part of the application creates an instance of Invoker, passing in an instance of Updateable. In my example, this will take place in the decideURL() method of URLProcessor_good. The Invoker constructor loads up the rules -- that is, the match-up between possible values of Conditions and instances of Action (I will explain more about Rules below.

This is all there is to initialization. Figure 2 provides a sequence diagram for these simple steps.

Figure 2. Initialization of framework classes

When it's time to evaluate the if-then-else logic and carry out the corresponding actions (for this example, when it is time to execute the logic in decideURL()), the application invokes the execute() method on Invoker, which loads up the conditions that reflect the current state, and fires off the framework engine, passing in the vectors of rules and conditions. The framework engine -- that is, the instance of the IfThenElse class -- uses its Rules instance to look up the appropriate instance of Action and requests it to fire its execute() method. Typically, execute() calls the doUpdate() method on the Updateable object. Figure 3 illustrates this process.

Figure 3. Execution of branching logic in the if-then-else framework

As mentioned earlier, I am using the term "rules" to refer to the match-up between a sequence of booleans (which represent a sequence of evaluated conditions) and a corresponding action. (As you will see in a moment, when defining these rules, you represent a sequence of booleans as a string of Ts and Fs, where T stands for true and F stands for false.)

Because rules are so important, I have encapsulated a set of rules in its own framework class called Rules. Here is a skeleton of the Rules class:

public class Rules {
    private Hashtable ruleTable;
    public void addRule(String boolStr, Action action)
        throws NestingTooDeepException {
    . . .
    }
    public Action lookUp(BooleanTuple bt) {
        . . .
    }
}

The two methods in the Rules class are addRule and lookUp. The addRule(String boolStr, Action action) method first converts a String boolStr of Ts and Fs to a sequence of Boolean instances, represented by the framework class BooleanTuple. The instance of BooleanTuple is then paired with the argument action in the Hashtable ruleTable.

The method lookUp(BooleanTuple bt) is the inverse operation. It looks up the instance of Action in ruleTable using the passed in BooleanTuple as a key.

In practice, when you implement the loadRules() method in your subclass of Invoker, you create an instance of Rules, and then make one call to addRule(..) for each relevant sequence of Ts and Fs and corresponding instance of Action.

Four steps to implement the if-then-else framework

The discussion so far has been abstract. To help make the ideas more concrete, I will outline a four-step procedure for implementing the if-then-else framework and apply it to the task of rewriting the decideURL() method in Listing 1. Here is an overview of the four steps:

  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) implement the Updateable interface (this means that 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.

Now I will tell you how to implement Step 1 to rewrite the decideURL() method. Next month, Part 2 will complete the discussion of the implementation for Steps 2 through 4.

1 2 3 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more