http://www.developer.com/

Back to article

Working with Design Patterns: Template Method


April 27, 2007

Design patterns are powerful tools. Powerful tools always present the potential for misuse. If we're not careful, we could chop off a leg or glue our code together into a stick mess. Template Method is a design pattern that presents one solution for eliminating common duplication between two or more related classes. It is one of the twenty-three patterns demonstrated in the book Design Patterns (1995, by Gamma, et. al.).

You're best off treating Template Method as a refactoring goal. In this article, you'll step through an example that shows how to manipulate code to meet that goal. Instead of talking about what Template Method even is, we'll just refactor and talk about the results when we're done.

For this example, we're building a rule-based workflow system for managing scanned forms. The first rule we tackle requires that the state supplied on a form match the state passed in as an argument. The class CheckStateRule implements the Rule interface:

import java.util.*;

public interface Rule {
   boolean eval(Case c, List<String> val, boolean returnTrue);
}
The workflow system executes rules. Executing a rule can move forms through the workflow.

The CheckStateRule code is fairly straightforward. The rule engine passes a Case object, a list of arguments, and a negation flag to the eval method. The Case indirectly contains form data. The switchReturnValues method, a slightly confusing bit, flips the result of the rule if someone has configured it in the workflow system to be negated.

import java.util.*;

/**
 *
 * Check if the state the case is located in is equal to argument passed in.
 */
public class CheckStateRule implements Rule {
   private static final String EXCEPT = "Exception: ";
   String errStr;
   boolean hasMissingData;

   public boolean eval(Case c, List<String> val, boolean returnTrue) {
      boolean returnVal = false;
      String argVal = null;

      try {
         if (val == null || val.size() != 1) {
            errStr = "Can't convert value list to a state";
            hasMissingData = true;
            return false;
         } else {
            argVal = (String)val.get(0);
         }

         // get the location object off the form and get the state the
         // case resides in
         Location loc = (Location)c.getFormComposite("location");
         if (loc == null) {
            errStr = "Cannot determine the state this case is located in.";
            hasMissingData = true;
            return false;
         }
         String st = loc.getAttribute("state");
         String state = (st == null ? null : st.toUpperCase());
         if (state == null) {
            errStr = "Cannot determine the state this case is located in.";
            hasMissingData = true;
            return false;
         } else if (state.equals(argVal)) {
            returnVal = true;
         }

         // return value
         List<Object> returns = switchReturnValues(returnTrue, returnVal,
               "Case does not reside in '" + argVal + "'", "Case resides in '"
                     + argVal + "'");
         returnVal = ((Boolean)returns.get(0)).booleanValue();
         errStr = (String)returns.get(1);

      } catch (Exception e) {
         errStr = EXCEPT + e.getMessage();
         hasMissingData = true;
         return false;
      }

      return returnVal;

   }

   public List<Object> switchReturnValues(boolean returnTrue,
         boolean returnVal, String falseMsg, String trueMsg)
         throws Exception {

      if (!returnTrue)
         returnVal = !returnVal;

      String retErrStr = null;
      if (!returnVal) {
         if (returnTrue)
            retErrStr = falseMsg;
         else
            retErrStr = trueMsg;
      }

      ArrayList<Object> returns = new ArrayList<Object>();
      returns.add(new Boolean(returnVal));
      returns.add(retErrStr);
      return returns;
   }

   public boolean isMissingData() {
      return hasMissingData;
   }

   public String getError() {
      return errStr;
   }
}

As soon as we get CheckStateRule working, we begin work on a second rule, CheckDependentsRule. The job of this second rule is to ensure that each dependent specified on the form has both a first and last name.

import java.util.*;

/**
 *
 * Ensure all dependent detail is supplied
 */
public class CheckDependentsRule implements Rule {
   private static final String EXCEPT = "Exception: ";
   private String errorMessage;
   private boolean missingData = false;

   public boolean eval(Case c, List<String> val, boolean returnTrue) {
      boolean returnVal = false;

      try {
         // get the location object off the form and get the state the
         // case resides in
         Subscriber sub = (Subscriber)c.getFormComposite("subscriber");
         if (sub == null) {
            errorMessage = "Unable to obtain subscriber info.";
            missingData = true;
            return false;
         }

         returnVal = true;
         for (int i = 1; i <= Subscriber.DEPENDENT_ROWS; i++) {
            if (!hasCompleteDependentName(sub, i)) {
               returnVal = false;
               break;
            }
         }

         boolean returnVal1 = returnVal;
         if (!returnTrue)
            returnVal1 = !returnVal1;

         String retErrStr = null;
         if (returnVal1 == false) {
            if (returnTrue)
               retErrStr = "Dependent info incomplete";
            else
               retErrStr = "All dependent info provided";
         }

         List<Object> res = new ArrayList<Object>();
         res.add(new Boolean(returnVal1));
         res.add(retErrStr);

         // return value
         List<Object> returns = res;
         returnVal = ((Boolean)returns.get(0)).booleanValue();
         errorMessage = (String)returns.get(1);

      } catch (Exception e) {
         errorMessage = EXCEPT + e.getMessage();

         missingData = true;
         return false;
      }

      return returnVal;

   }

   private boolean hasCompleteDependentName(Subscriber sub, int number) {
      String firstName = sub.getAttribute("dep" + number + "first");
      String lastName = sub.getAttribute("dep" + number + "last");

      if ((!isEmpty(firstName) && isEmpty(lastName)) ||
            (!isEmpty(lastName) && isEmpty(firstName))) {
         errorMessage = "Complete name must be specified for dependent " + number;
         return false;
      }
      return true;
   }

   private boolean isEmpty(String string) {
      return string == null || string.equals("");
   }

   public boolean isMissingData() {
      return missingData;
   }

   public String getError() {
      return errorMessage;
   }
}

The two rules, CheckDependentsRule and CheckStateRule, appear to have some similarities. Chunks of the eval method are the same, and a few supporting methods appear to be the same. We want to eliminate the redundancies between these two classes, which should reduce our risk and future maintenance costs.

Should we use delegation or inheritance? Normally I prefer to start by considering delegation, as it doesn't introduce as-strong dependencies. Delegation is ideal when we need to re-use common constructs and logic. But in this case, our two classes are behaviorally related. Both share the same goals and behaviors. We'll create a common class, BaseRule, and slowly pull common functionality up into it.

As the first step, we simply create an empty superclass and have both Rule classes extend it. We then, naturally, run our tests.

public class BaseRule {
}

public class CheckStateRule extends BaseRule implements Rule {
...

public class CheckDependentsRule extends BaseRule implements Rule {
...

If we look at CheckDependentsRule, the last chunk of code in the try block appears to replicate the functionality in CheckStateRule's switchReturnValues method.

         boolean returnVal1 = returnVal;
         if (!returnTrue)
            returnVal1 = !returnVal1;

         String retErrStr = null;
         if (returnVal1 == false) {
            if (returnTrue)
               retErrStr = "Dependent info incomplete";
            else
               retErrStr = "All dependent info provided";
         }

         List<Object> res = new ArrayList<Object>();
         res.add(new Boolean(returnVal1));
         res.add(retErrStr);
We extract this code to a separate method with the same name as in CheckStateRule:
         // return value
         List<Object> returns = switchReturnValues(returnTrue, returnVal,
               "Dependent info incomplete", "All dependent info provided");
         ...
   }

   private List<Object> switchReturnValues(boolean returnTrue, boolean returnVal, String trueMsg, String falseMsg) {
      boolean returnVal1 = returnVal;
      if (!returnTrue)
         returnVal1 = !returnVal1;

      String retErrStr = null;
      if (returnVal1 == false) {
         if (returnTrue)
            retErrStr = trueMsg;
         else
            retErrStr = falseMsg;
      }

      List<Object> res = new ArrayList<Object>();
      res.add(new Boolean(returnVal1));
      res.add(retErrStr);
      return res;
   }

At this point we can visually inspect the two switchReturnValues methods and see if they indeed contain the same logic. Since we have good unit tests, we can spend only a little bit of time on this comparision, enough to get a good warm fuzzy. The real answer comes by trying to eliminate the redundancy: We pull one of them up to the BaseRule superclass (run tests), and eliminate the other (run tests again). All remains green.

import java.util.*;

public class BaseRule {
   protected List<Object> switchReturnValues(boolean returnTrue,
         boolean returnVal, String trueMsg, String falseMsg) {
      boolean returnVal1 = returnVal;
      if (!returnTrue)
         returnVal1 = !returnVal1;

      String retErrStr = null;
      if (returnVal1 == false) {
         if (returnTrue)
            retErrStr = trueMsg;
         else
            retErrStr = falseMsg;
      }

      List<Object> res = new ArrayList<Object>();
      res.add(new Boolean(returnVal1));
      res.add(retErrStr);
      return res;
   }
}

Getting rid of duplication often involves making things look the same. In both rule classes, we have a hasMissingData flag. We also have a String field that appears to hold an error message, but its name is different. We rename the field in CheckStateRule from errStr to errorMessage (and run tests). We also make both fields private in CheckStateRule, since they are both private in CheckDependentsRule. We run our tests. We do the same thing with the hasMissingData/missingData fields.

Once we complete the preparatory work of making things look the same, we pull up both the fields and their accessor (getter) methods to BaseRule. We make the fields protected instead of private. This is perhaps a questionable design decision, but we can look to correct it once the rest of our current refactoring is complete. We verify that our tests still pass.

One more simple refactoring: both classes define an EXCEPT constant. We pull this definition to the superclass and eliminate from both subclasses.

Now the fun work begins. The eval method is still a bit long. But if we look at the structure of the eval method within the CheckStateRule, we note that it has the following policy:

  • extract arg values
  • extract form data
  • execute rule
  • prepare return values, negating if necessary
  • handle any trapped exception from the above three steps as a rule failure

CheckDependentsRule has pretty much the same structure, except that it doesn't extract any argument values. Starting with CheckStateRule, we break up the function into method calls that correspond to this policy.

import java.util.*;

/**
 *
 * Check if the state the case is located in is equal to argument passed in.
 */
public class CheckStateRule extends BaseRule implements Rule {
   private String argVal;
   private String formState;

   public boolean eval(Case c, List<String> val, boolean returnTrue) {
      try {
         return evalWithThrow(c, val, returnTrue);
      } catch (Exception e) {
         errorMessage = BaseRule.EXCEPT + e.getMessage();
         hasMissingData = true;
         return false;
      }
   }

   private boolean evalWithThrow(Case c, List<String> val, boolean returnTrue) {
      if (!extractArgs(val))
         return false;
      if (!extractFormData(c))
         return false;
      boolean returnVal = executeRule(c);
      return prepareReturnValues(returnTrue, returnVal);
   }

   private boolean extractFormData(Case c) {
      // get the location object off the form and get the state the
      // case resides in
      Location loc = (Location)c.getFormComposite("location");
      if (loc == null) {
         errorMessage = "Cannot determine the state this case is located in.";
         hasMissingData = true;
         return false;
      }
      String st = loc.getAttribute("state");
      formState = (st == null ? null : st.toUpperCase());
      if (formState == null) {
         errorMessage = "Cannot determine the state this case is located in.";
         hasMissingData = true;
         return false;
      }
      return true;
   }

   private boolean prepareReturnValues(boolean returnTrue, boolean returnVal) {
      List<Object> returns = switchReturnValues(returnTrue, returnVal,
            "Case does not reside in '" + argVal + "'", "Case resides in '"
                  + argVal + "'");
      returnVal = ((Boolean)returns.get(0)).booleanValue();
      errorMessage = (String)returns.get(1);
      return returnVal;
   }

   private boolean executeRule(Case c) {
      return formState.equals(argVal);
   }

   private boolean extractArgs(List<String> val) {
      if (val == null || val.size() != 1) {
         errorMessage = "Can't convert value list to a state";
         hasMissingData = true;
         return false;
      }
      argVal = (String)val.get(0);
      return true;
   }
}

The methods eval and evalWithThrow provide the definitions of policy. Of course we make these changes incrementally. We also make typical simple mistakes along the way. Running the tests continuously keeps these mistakes from getting buried and becoming a big nuisance.

We can now pull up the eval and evalWithThrow policy methods to BaseRule. That creates complie errors. In order to get BaseRule compiling, we create stubs for things that are specific to CheckStateRule (for example, extractFormData). We change the stubs to protected in both BaseRule and CheckStateRule. Tests still run.

   public boolean eval(Case c, List<String> val, boolean returnTrue) {
      try {
         return evalWithThrow(c, val, returnTrue);
      } catch (Exception e) {
         errorMessage = BaseRule.EXCEPT + e.getMessage();
         hasMissingData = true;
         return false;
      }
   }

   private boolean evalWithThrow(Case c, List<String> val, boolean returnTrue) {
      if (!extractArgs(val))
         return false;
      if (!extractFormData(c))
         return false;
      boolean returnVal = executeRule(c);
      return prepareReturnValues(returnTrue, returnVal);
   }


   protected boolean prepareReturnValues(boolean returnTrue, boolean returnVal) {
      return false;
   }

   protected boolean executeRule(Case c) {
      return false;
   }

   protected boolean extractFormData(Case c) {
      return false;
   }

   protected boolean extractArgs(List<String> val) {
      return false;
   }

In the next step, we convert the stub methods into abstract methods (which makes BaseRule abstract). Each such change causes compile errors in CheckDependentsRule. Rather than declare all methods abstract simultaneously, which would generate more than a few minutes work before we could see a green bar, we do them one at a time.

Finally, when done, we take a bold step and delete the eval method in CheckDependentsRule. Our resulting code:

import java.util.*;

/**
 *
 * Ensure all dependent detail is supplied
 */
public class CheckDependentsRule extends BaseRule implements Rule {
   private Subscriber sub;

   @Override
   protected boolean prepareReturnValues(boolean returnTrue, boolean returnVal) {
      List<Object> returns = switchReturnValues(returnTrue, returnVal,
            "Dependent info incomplete", "All dependent info provided");
      returnVal = ((Boolean)returns.get(0)).booleanValue();
      errorMessage = (String)returns.get(1);
      return returnVal;
   }

   @Override
   protected boolean executeRule(Case c) {
      for (int i = 1; i <= Subscriber.DEPENDENT_ROWS; i++) {
         if (!hasCompleteDependentName(sub, i)) {
            return false;
         }
      }

      return true;
   }

   private boolean hasCompleteDependentName(Subscriber sub, int number) {
      String firstName = sub.getAttribute("dep" + number + "first");
      String lastName = sub.getAttribute("dep" + number + "last");

      if ((!isEmpty(firstName) && isEmpty(lastName)) ||
            (!isEmpty(lastName) && isEmpty(firstName))) {
         errorMessage = "Complete name must be specified for dependent " + number;
         return false;
      }
      return true;
   }

   @Override
   protected boolean extractFormData(Case c) {
      sub = (Subscriber)c.getFormComposite("subscriber");
      if (sub == null) {
         errorMessage = "Unable to obtain subscriber info.";
         hasMissingData = true;
         return false;
      }

      return true;
   }

   @Override
   protected boolean extractArgs(List<String> val) {
      return true;
   }

   private boolean isEmpty(String string) {
      return string == null || string.equals("");
   }

}

The final version of BaseRule:

import java.util.*;

abstract public class BaseRule {
   protected String errorMessage;
   protected boolean hasMissingData = false;
   static final String EXCEPT = "Exception: ";

   protected List<Object> switchReturnValues(boolean returnTrue,
         boolean returnVal, String trueMsg, String falseMsg) {
      boolean returnVal1 = returnVal;
      if (!returnTrue)
         returnVal1 = !returnVal1;

      String retErrStr = null;
      if (returnVal1 == false) {
         if (returnTrue)
            retErrStr = trueMsg;
         else
            retErrStr = falseMsg;
      }

      List<Object> res = new ArrayList<Object>();
      res.add(new Boolean(returnVal1));
      res.add(retErrStr);
      return res;
   }

   public boolean eval(Case c, List<String> val, boolean returnTrue) {
      try {
         return evalWithThrow(c, val, returnTrue);
      } catch (Exception e) {
         e.printStackTrace();
         errorMessage = BaseRule.EXCEPT + e.getMessage();
         hasMissingData = true;
         return false;
      }
   }

   private boolean evalWithThrow(Case c, List<String> val, boolean returnTrue) {
      if (!extractArgs(val))
         return false;
      if (!extractFormData(c))
         return false;
      boolean returnVal = executeRule(c);
      return prepareReturnValues(returnTrue, returnVal);
   }

   abstract protected boolean prepareReturnValues(boolean returnTrue, boolean returnVal);

   abstract protected boolean executeRule(Case c);

   abstract protected boolean extractFormData(Case c);

   abstract protected boolean extractArgs(List<String> val);

   public boolean isMissingData() {
      return hasMissingData;
   }

   public String getError() {
      return errorMessage;
   }
}
And, for completeness sake, the final version of CheckStateRule:
import java.util.*;

/**
 *
 * Check if the state the case is located in is equal to argument passed in.
 */
public class CheckStateRule extends BaseRule implements Rule {
   private String argVal;
   private String formState;

   protected boolean extractFormData(Case c) {
      // get the location object off the form and get the state the
      // case resides in
      Location loc = (Location)c.getFormComposite("location");
      if (loc == null) {
         errorMessage = "Cannot determine the state this case is located in.";
         hasMissingData = true;
         return false;
      }
      String st = loc.getAttribute("state");
      formState = (st == null ? null : st.toUpperCase());
      if (formState == null) {
         errorMessage = "Cannot determine the state this case is located in.";
         hasMissingData = true;
         return false;
      }
      return true;
   }

   protected boolean prepareReturnValues(boolean returnTrue, boolean returnVal) {
      List<Object> returns = switchReturnValues(returnTrue, returnVal,
            "Case does not reside in '" + argVal + "'", "Case resides in '"
                  + argVal + "'");
      returnVal = ((Boolean)returns.get(0)).booleanValue();
      errorMessage = (String)returns.get(1);
      return returnVal;
   }

   protected boolean executeRule(Case c) {
      return formState.equals(argVal);
   }

   protected boolean extractArgs(List<String> val) {
      if (val == null || val.size() != 1) {
         errorMessage = "Can't convert value list to a state";
         hasMissingData = true;
         return false;
      }
      argVal = (String)val.get(0);
      return true;
   }
}

We could make a few additional changes to clean up the subclasses. The method prepareReturnValues looks like it contains some redundancies between CheckStatesRule and CheckDependentsRule. We'll leave that cleanup work as an exercise for the reader.

The "policy" method in eval and also evalWithThrow are also referred to as template methods. When applying Template Method, we provide a common superclass method that represents an algorithm, or template, for behavior.

Certain portions of a template algorithm need to vary by subclass (CheckDependentsRule and CheckStateRule in our example). We provide the entire algorithm in the template method, but leave "holes" in it. We implement a hole as a call to an abstract method defined on the base class. Subclasses must fill in the holes.

The downside of Template Method is that it can obscure details to someone looking only at the subclass implementations. To fully understand what CheckDependentsRule does, a reader must have a good conceptual idea of how things are laid out in BaseRule. The price of eliminating duplication is increased abstraction, which increases comprehension costs. We want to constantly revisit method names when applying Template Method, and strive for the best way to explain how the subclass fills in the holes.

Template Method is one of those patterns that we can easily abuse by overuse. Rather than approach a problem and think that Template Method is the way to solve it, we're better off letting test-driven development guide us. Sometimes, we'll refactor toward Template Method, but other times we'll find that it isn't the best solution.

About the Author

Jeff Langr is a veteran software developer with a score and more years of experience. He's authored two books and dozens of published articles on software development, including Agile Java: Crafting Code With Test-Driven Development (Prentice Hall) in 2005. You can find out more about Jeff at his site, http://langrsoft.com, or you can contact him directly at jeff@langrsoft.com.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date