Validation belongs in a Model Object

An application should perform data validation in a Model Object, where it can validate both user input, and the data being fetched from a database. This validation should be performed in a class, and not in an .xml configuration file or some similar tool.

The role of a Model Object is to implement business logic, and validation is the single most common (and arguably the most important) kind of business logic. The fundamental reason for creating any class is often stated as bringing together data and closely related operations on that data. This is often the first idea taught in courses on object programming.

Surprisingly, there are many web presentation frameworks that do not follow this simple guiding principle of lasting value. Instead, they encourage the application programmer to separate data and logic, by doing validation either in Java Server Pages, or in configuration files (typically an .xml file). What is the justification for this? There does not seem to be any. If the reason is that "it allows validation to be configured", then this seems like a dubious argument. Is it the job of a deployer or a page author to make decisions regarding important business logic? No, it's not. On the contrary, for most organizations, allowing such casual changes to business logic in a production environment is regarded as highly undesirable, and is thus an argument against such techniques, not for them.

Fundamentally, validation is a programming task, and it always will be: it involves logic. It's not a presentation task, and it's not a deployment task. Presentation frameworks which provide validation tools are "leaking over" into the domain of business logic, in an apparent attempt to increase their list of features. However, this style is inappropriate for the application programmer, since it breaks a fundamental rule of object programming. Thus, such style of validation should likely be avoided.

Implementing validation in Model Objects is simple and natural. As usual, any common validations can be factored out into utility classes. In addition, writing test cases is simple, and can be executed stand-alone, outside of the web environment.

Example

To assist the application programmer with common validations, the WEB4J tool defines a Validator interface, and a Check class which returns some common implementations of that interface. Validation is implemented entirely in code, and never with JSPs or configuration files. Here, a Resto Model Object uses Check to perform all required validation in its constructor, by calling validateState:

package hirondelle.fish.main.resto;

import hirondelle.web4j.model.ModelCtorException;
import hirondelle.web4j.model.ModelUtil;
import hirondelle.web4j.model.Id;
import hirondelle.web4j.security.SafeText;
import hirondelle.web4j.model.Decimal;
import static hirondelle.web4j.model.Decimal.ZERO;
import hirondelle.web4j.model.Check;
import hirondelle.web4j.model.Validator;
import static hirondelle.web4j.util.Consts.FAILS;

/** Model Object for a Restaurant. */
public final class Resto {

  /**
   Full constructor.
    
   @param aId underlying database internal identifier (optional) 1..50 characters
   @param aName of the restaurant (required), 2..50 characters
   @param aLocation street address of the restaurant (optional), 2..50 characters
   @param aPrice of the fish and chips meal (optional) $0.00..$100.00
   @param aComment on the restaurant in general (optional) 2..50 characters
  */
  public Resto(
    Id aId, SafeText aName, SafeText aLocation, Decimal aPrice, SafeText aComment
  ) throws ModelCtorException {
    fId = aId;
    fName = aName;
    fLocation = aLocation;
    fPrice = aPrice;
    fComment = aComment;
    validateState();
  }
  
  public Id getId() { return fId; }
  public SafeText getName() {  return fName; }
  public SafeText getLocation() {  return fLocation;  }
  public Decimal getPrice() { return fPrice; }
  public SafeText getComment() {  return fComment; }

  @Override public String toString(){
    return ModelUtil.toStringFor(this);
  }
  
  @Override public  boolean equals(Object aThat){
    Boolean result = ModelUtil.quickEquals(this, aThat);
    if (result ==  null) {
      Resto that = (Resto) aThat;
      result = ModelUtil.equalsFor(
        this.getSignificantFields(), that.getSignificantFields()
      );
    }
    return result;
  }
  
  @Override public int hashCode(){
    if (fHashCode == 0){
      fHashCode = ModelUtil.hashCodeFor(getSignificantFields());
    }
    return fHashCode;
  }
  
  // PRIVATE 
  private final Id fId;
  private final SafeText fName;
  private final SafeText fLocation;
  private final Decimal fPrice;
  private final SafeText fComment;
  private int fHashCode;
  
  private static final Decimal HUNDRED = Decimal.from("100");

  private void validateState() throws ModelCtorException {
    ModelCtorException ex = new ModelCtorException();
    if (FAILS == Check.optional(fId, Check.range(1,50))) {
      ex.add("Id is optional, 1..50 chars.");
    }
    if (FAILS == Check.required(fName, Check.range(2,50))) {
      ex.add("Restaurant Name is required, 2..50 chars.");
    }
    if (FAILS == Check.optional(fLocation, Check.range(2,50))) {
      ex.add("Location is optional, 2..50 chars.");
    }
    Validator[] priceChecks = {Check.range(ZERO, HUNDRED), Check.numDecimalsAlways(2)};
    if (FAILS == Check.optional(fPrice, priceChecks)) {
      ex.add("Price is optional, 0.00 to 100.00.");
    }
    if (FAILS == Check.optional(fComment, Check.range(2,50))) {
      ex.add("Comment is optional, 2..50 chars.");
    }
    if ( ! ex.isEmpty() ) throw ex;
  }
  
  private Object[] getSignificantFields(){
    return new Object[] {fName, fLocation, fPrice, fComment};
  }
}
 

See Also :
JSPs should contain only presentation logic
Model Objects