Command objects

It's a common practice to define an interface for processing a user's request at a high level of abstraction. This corresponds roughly to the Command pattern described in Design Patterns.

Example

This example is in the context of a web application. Action is an interface which:

package hirondelle.web4j.action;

import hirondelle.web4j.model.AppException;

/**
 <span class="highlight">
 Process an HTTP request, and return a {@link ResponsePage}.
 </span>

 <P><b>This interface is likely the most important 
 abstraction in WEB4J.</b> Almost every feature implemented by the programmer will 
 need an implementation of this interface. 
 
 <P>Typically, one of the <em>ActionXXX</em> abstract base classes are used to 
 build implementations of this interface.
*/
public interface Action {
  
  /**
  Execute desired operation.
   
  <P>Typical operations include : 
  <ul>
  <li>validate user input
  <li>interact with the database 
  <li>place required objects in the appropriate scope
  <li>set the appropriate {@link ResponsePage}.
  </ul>
  
  <P>Returns an identifier for the resource (for example a JSP) which 
  will display the end result of this <tt>Action</tt> (using either a 
  forward or a redirect).
  */
  ResponsePage execute() throws AppException;
  
}
 

Action implementations perform similar tasks. These tasks should be placed in an Abstract Base Class (ABC), which will simplify concrete implementations. The WEB4J tool defines several such ABC's:

The various ActionTemplateXXX classes implement the template method design pattern. The application programmer chooses a particular template based in the desired style of user interface.

Here's an example of a concrete Action, which subclasses the ActionTemplateShowAndApply class mentioned above. It provides implementations for three methods: show, validateUserInput, and apply.


package hirondelle.fish.main.discussion;

import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.action.ActionTemplateShowAndApply;
import hirondelle.web4j.action.ResponsePage;
import hirondelle.web4j.database.DAOException;
import hirondelle.web4j.database.Db;
import hirondelle.web4j.database.SqlId;
import hirondelle.web4j.model.AppException;
import hirondelle.web4j.model.ModelCtorException;
import hirondelle.web4j.model.ModelFromRequest;
import hirondelle.web4j.request.RequestParameter;
import hirondelle.web4j.request.RequestParser;

import java.util.Date;
import java.util.List;
import java.util.regex.Pattern;

/**
 List comments, and add new ones.
 
 <P>Comments are listed, along with a paging mechanism.
 
 @sql statements.sql
 @view view.jsp
*/
public final class CommentAction extends ActionTemplateShowAndApply {
  
  public static final SqlId FETCH_RECENT_COMMENTS =  new SqlId("FETCH_RECENT_COMMENTS");
  public static final SqlId ADD_COMMENT =  new SqlId("ADD_COMMENT");
  
  /** Constructor. */
  public CommentAction(RequestParser aRequestParser){
    super(FORWARD, REDIRECT, aRequestParser);
  }
  
  public static final RequestParameter COMMENT_BODY = RequestParameter.withLengthCheck(
    "Comment Body"
   );

  /** Used for the paging mechanism. */
  public static final RequestParameter PAGE_SIZE = RequestParameter.withRegexCheck(
    "PageSize", Pattern.compile("(\\d){1,4}")
   );
  /** Used for the paging mechanism. */
  public static final RequestParameter PAGE_INDEX = RequestParameter.withRegexCheck(
    "PageIndex", Pattern.compile("(\\d){1,4}")
  );
  
  /** Show the listing of comments, and a form for adding new messages. */
  protected void show() throws AppException {
    addToRequest(ITEMS_FOR_LISTING, fetchRecentComments());
  }
  
  /** Ensure user input can build a new {@link Comment}.  */
  protected void validateUserInput() throws AppException {
    try {
      ModelFromRequest builder = new ModelFromRequest(getRequestParser());
      /*
        This is an example of using a time which, for testing purposes,
        can be made independent of the true system time. The value of 
        the 'now' variable depends on the implementation of TimeSource.
       */
      long now = BuildImpl.forTimeSource().currentTimeMillis();
      fComment = builder.build(
        Comment.class, getLoggedInUserName(), COMMENT_BODY, new Date(now)
      );
    }
    catch (ModelCtorException ex){
      addError(ex);
    }
  }
  
  /** Add a new {@link Comment} to the database. */
  protected void apply() throws AppException {
    //no possibility of a duplicate error.
    addNew(fComment);
  }
  
  // PRIVATE //
  private Comment fComment;
  
  private static final ResponsePage FORWARD = new ResponsePage(
    "Discussion", "view.jsp", CommentAction.class
  );
  private static final ResponsePage REDIRECT =  new ResponsePage(
    "CommentAction.show?PageIndex=1&PageSize=10"
  );
  
  /*
   Here, the DAO methods are not in a separate class, but simply regular methods of 
   this Action. This reasonable since the methods are short, and they do not make 
   this Action class significantly more complex. 
  */
  
  /**
   Return an immutable {@link List} of recent {@link Comment} objects.
  
   <P>The definition of what constitutes "recent" is left deliberately vague, to 
   allow various versions of "recent" - last 5 messages, messages entered over 
   the last N days, etc.
  */
  private List<Comment> fetchRecentComments() throws DAOException {
    return Db.list(Comment.class, FETCH_RECENT_COMMENTS);
  }

  /**
   Add <tt>aNewComment</tt> to the database. No duplicates are possible.
  
   @param aNewComment to be added to the datastore.
  */
  private void addNew(Comment aNewComment) throws DAOException {
    Object[] params = { 
      aNewComment.getUserName(), aNewComment.getBody(), aNewComment.getDate() 
    };
    Db.edit(ADD_COMMENT, params);
  }
}
 



See Also :
Refactor large Controllers
Parse parameters into domain objects
Template method
Forward versus redirect
A Web App Framework - WEB4J
Would you use this technique?
Yes   No   Undecided   
© 2014 Hirondelle Systems | Source Code | Contact | License | RSS
Individual code snippets can be used under this BSD license - Last updated on September 21, 2013.
Over 2,000,000 unique IPs last year - Built with WEB4J.
- In Memoriam : Bill Dirani -