April 17, 2014
Hot Topics:
RSS RSS feed Download our iPhone app

Working with Design Patterns: Template Method, Page 3

  • April 27, 2007
  • By Jeff Langr
  • Send Email »
  • More Articles »

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.



Page 3 of 3



Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Sitemap | Contact Us

Rocket Fuel