Template method

Template methods are perhaps the most commonly used of all design patterns. They define the general steps of a computation or algorithm, while deferring the implementation of at least one of the steps.

Example 1

In older versions of Java, prior to Java 8, the template method pattern was implemented with an Abstract Base Class (see Example 2 below). But in Java 8 or later, the preferred way of implementing the template method pattern is to use functional programming. Using a function pointer agrees with one of the core rules of design patterns - "Favor object composition over class inheritance." That is, it's usually better to avoid inheritance (using the extends keyword), if possible.

This example is about the rating of stocks by an investment analyst. Here, the exact algorithm for rating a stock is treated as a parameter. In the example, StockInfo.strongestRecommendationIn() is the template method. The important point is that it takes a Function object as a parameter, which points to a specific algorithm for rating stocks. So, different callers can pass different algorithms.

This example is a bit contrived, since all it really does is fetch the necessary data related to the stock, before applying the stock-rating algorithm.

import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;

import java.math.BigDecimal;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;

/** @since Java 8 */
public final class StockInfo {
  
  /** 
   Pick the best stock to buy, given a Set of stocks.
   The returned Set usually has a single element. In the case of a tie, 
   it will contain more than 1 element.

   @param stocks the set of stocks being analyzed.
   @param toRecommendation the stock analysis algorithm.
   @return an empty Set only if stocks is itself empty.
  */
  public Set<Recommendation> strongestRecommendationIn(
    Set<Stock> stocks, Function<Stock, Recommendation> toRecommendation
  ){
    Set<Recommendation> result = Collections.emptySet();
    if (!stocks.isEmpty()){
      //gather data related to the stocks, presumably by talking to a database
      int FOR_A_TEN_YEAR_PERIOD = 10;
      for(Stock stock : stocks){
        addCoreCompanyInfo(stock);
        addQuarterlyReports(stock, FOR_A_TEN_YEAR_PERIOD);
        addMarketPrice(stock);
      }
      
      //now that all of the data has been gathered together, 
      //do the analysis of the stocks; return all that 
      //share the max score
      
      //this fails to handle ties, so we can't use it here
      Optional<Recommendation> best = stocks
        .stream()
        .map(toRecommendation)
        .max(Recommendation::compareTo)
      ;
      if (best.isPresent()){
        log("Best (no ties found):" + best.get());
      }
      
      //we need a data structure that has an order, so we use List, not Set
      List<Recommendation> allRecs = stocks
        .stream()
        .map(toRecommendation)
        .collect(toList())
      ;
      Collections.sort(allRecs, Collections.reverseOrder()); //Recommendation.compareTo
      Integer topScore = allRecs.get(0).getScore();
      log("Best :" + topScore);
      result = allRecs
        .stream()
        .filter(rec -> rec.getScore().equals(topScore))
        .collect(toSet())
      ;
      log(result);
    }
    return result;
  }

  /** Puts data into the stock object. */
  public void addCoreCompanyInfo(Stock stock){
    //..elided
  };
  
  /** Puts data into the stock object. */
  public void addQuarterlyReports(Stock stock, Integer numYears){
    //..elided
  } 
  
  /** Puts data into the stock object. */
  public void addMarketPrice(Stock stock){
    //..elided
  }
  
  /** Informal test. */
  public static void main(String... args) {
    Set<Stock> stocks = new LinkedHashSet<>();
    stocks.add(new Stock("T"));
    stocks.add(new Stock("ENB"));
    stocks.add(new Stock("BNS"));
    stocks.add(new Stock("CM"));
    stocks.add(new Stock("KO"));
    
    StockInfo stockInfo = new StockInfo();
    Set<Recommendation> best = stockInfo.strongestRecommendationIn(
      stocks, StockInfo::toyAnalyzer
    );
    
    log("Stocks: " + stocks, "Best: " + best);
  }
  
  /** 
   This is a silly way to score stocks, based on the length of its name!
   In the real world, it would depend on a large data set. 
  */
  private static Recommendation toyAnalyzer(Stock stock){
    Recommendation result = new Recommendation(
      stock, stock.getSymbol().length(), new BigDecimal("10.0")
    );
    return result;
  }
  
  private static void log(Object... msgs){
    Stream.of(msgs).forEach(System.out::println);
  }

} 

/** Simply holds data related to a stock. */
public final class Stock {
  
  public Stock(String symbol){
    this.symbol = symbol;
  }
  
  public String getSymbol(){ 
    return symbol; 
  }
  @Override public String toString(){
    return symbol;
  }
  
  //..plus a lot of other data, not included here
  
  //..elided
  
  // PRIVATE 
  
  private String symbol;
}
 

import java.math.BigDecimal;

/** A stock analyst's recommendation for a stock. */
public final class Recommendation implements Comparable<Recommendation>{
  
  public Recommendation(
    Stock stock, Integer score, BigDecimal maxPercentOfHoldings
  ){
    this.stock = stock;
    this.score = score;
    this.maxPercentOfHoldings = maxPercentOfHoldings;
  }

  /** 
   Some range, such as -10..+10.
   Buy-sell-hold recommendations could be encoded here, 
   as parts of the range. 
  */
  public Integer getScore(){
    return score;
  }

  public BigDecimal getMaxPercentOfHoldings(){
    return maxPercentOfHoldings;
  }
  
  public Stock getStock(){
    return stock;
  }
  
  @Override public int compareTo(Recommendation that) {
    return this.score.compareTo(that.score);
  }
  
  @Override public String toString(){
    return 
      stock.toString() + 
      ": score=" + score + 
      " max-%-of-holdings=" + maxPercentOfHoldings
    ;
  }
  
  //..elided
  
  //PRIVATE 
  
  private Stock stock;
  private Integer score;
  private BigDecimal maxPercentOfHoldings;
} 

Example 2

This is an extended example of the older way of implementing a Template method. It's appropriate when working with older code, before Java 8.

This style uses an Abstract Base Class (ABC) with a single abstract method. The ABC defines the general steps of an algorithm, and most of its implementation. It defers one step in the algorithm to its subclasses. Many ABC's use the template method pattern.

TxTemplate is an abstract base class which defines a template method for executing multiple database operations within a transaction. It's useful to define these steps in one place. The alternative is to repeat the same structure every time a transaction is required. As usual, such code repetition should always be aggressively eliminated.

The executeTx method is the template method. It's final, and defines the general outline of how to execute a database transaction. The specific database actions to be taken are implemented by calling the abstract method executeMultipleSqls.

import java.sql.*;
import java.util.logging.*;

import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.util.Consts;

/** 
* Template for executing a local, non-distributed transaction versus a 
* single database, using a single connection.
*
* <P>This abstract base class implements the template method design pattern.
*/
public abstract class TxTemplate implements Tx {
  
  //..elided
  
  /**
  * <b>Template</b> method calls the abstract method {@link #executeMultipleSqls}.
  * <P>Returns the same value as <tt>executeMultipleSqls</tt>.
  *
  * <P>A <tt>rollback</tt> is performed if <tt>executeMultipleSqls</tt> fails.
  */
  public final int executeTx() throws DAOException {
    int result = 0;
    fLogger.fine(
      "Editing within a local transaction, with isolation level : " + fTxIsolationLevel
    );
    ConnectionSource connSource = BuildImpl.forConnectionSource();
    if(Util.textHasContent(fDatabaseName)){
      fConnection = connSource.getConnection(fDatabaseName);
    }
    else {
      fConnection = connSource.getConnection();
    }
    
    try {
      TxIsolationLevel.set(fTxIsolationLevel, fConnection);
      startTx();
      result = executeMultipleSqls(fConnection);
      endTx(result);
    }
    catch(SQLException rootCause){
      fLogger.fine("Transaction throws SQLException.");
      rollbackTx();
      String message = 
        "Cannot execute edit. ErrorId code : " +  rootCause.getErrorCode() + 
        Consts.SPACE + rootCause
      ;
      if (rootCause.getErrorCode() == DbConfig.getErrorCodeForDuplicateKey().intValue()){
        throw new DuplicateException(message, rootCause);
      }
      throw new DAOException(message, rootCause);
    }
    catch (DAOException ex){
      fLogger.fine("Transaction throws DAOException.");
      rollbackTx();
      throw ex;
    }
    finally {
      DbUtil.logWarnings(fConnection);
      DbUtil.close(fConnection);
    }
    fLogger.fine("Total number of edited records: " + result);
    return result;
  }

  /**
  * Execute multiple SQL operations in a single local transaction.
  *
  * <P>This method returns the number of records edited. 
  */
  public abstract int executeMultipleSqls(
    Connection aConnection
  ) throws SQLException, DAOException;
  
  // PRIVATE
  
  private Connection fConnection;
  private String fDatabaseName;
  private final TxIsolationLevel fTxIsolationLevel;
  
  private static final boolean fOFF = false;
  private static final boolean fON = true;
  
  private static final Logger fLogger = Util.getLogger(TxTemplate.class);  

  private void startTx() throws SQLException {
    fConnection.setAutoCommit(fOFF);
  }
  
  private void endTx(int aNumEdits) throws SQLException, DAOException {
    if ( BUSINESS_RULE_FAILURE == aNumEdits ) {
      fLogger.severe("Business rule failure occured. Cannot commit transaction.");
      rollbackTx();
    }
    else {
      fLogger.fine("Commiting transaction.");
      fConnection.commit();
      fConnection.setAutoCommit(fON);
    }
  }
  
  private void rollbackTx() throws DAOException {
    fLogger.severe("ROLLING BACK TRANSACTION.");
    try {
      fConnection.rollback();
    }
    catch(SQLException ex){
      throw new DAOException("Cannot rollback transaction", ex);
    }
  }
}
 

Here's an example of using TxTemplate. It alters the set of roles attached to an end user, first by deleting all existing roles, and then by adding the new roles one at a time.
final class RoleDAO {

  //..elided  

  /**
  * Update all roles attached to a user.
  * 
  * <P>This implementation will treat all edits to user roles as 
  * '<tt>DELETE-ALL</tt>, then <tt>ADD-ALL</tt>' operations. 
  */
  boolean change(UserRole aUserRole) throws DAOException {
    Tx update = new UpdateTransaction(aUserRole);
    return Util.isSuccess(update.executeTx());
  }
  
  // PRIVATE //
  
  /** Cannot be a {@link hirondelle.web4j.database.TxSimple}, since there is looping. */
  private static final class UpdateTransaction extends TxTemplate {
    UpdateTransaction(UserRole aUserRole){
      super(ConnectionSrc.ACCESS_CONTROL);
      fUserRole = aUserRole;
    }
    public int executeMultipleSqls(
      Connection aConnection
    ) throws SQLException, DAOException {
      int result = 0;
      //perform edits using a shared connection
      result = result + DbTx.edit(aConnection, ROLES_DELETE, fUserRole.getUserName());
      for(Id roleId : fUserRole.getRoles()){
        result = result + DbTx.edit(aConnection,ROLES_ADD,fUserRole.getUserName(),roleId);
      }
      return result;
    }
    private UserRole fUserRole;
  }
}
 

See Also :
Consider composition instead of subclassing
Command objects
Wrapper (Decorator)