Model Objects

The term Model Object is an informal term, with no widely accepted definition. Here, Model Objects (MOs) refer to data-centric classes which encapsulate closely related items.

Model Objects:

Should Model Objects follow the JavaBeans conventions?

Not necessarily. In fact, some argue that the JavaBeans style is to be avoided as a general model for Model Objects.

Should Model Objects be immutable?

Given the deep simplicity of immutable objects, some prefer to design their Model Objects as immutable. However, when the underlying data changes, a new object must be created, instead of simply calling a setXXX method on an existing object. Some argue that this penalty is too high, while others argue that it is a micro-optimization - especially in cases where the data is "read-mostly", and the state of corresponding Model Objects changes only rarely.

Implementing Model Objects as immutable seems particularly natural in web applications. There, Model Objects are most commonly placed in request scope, not session scope. In this case, there is no long-lived object for the user to directly alter, so the Model Object can be immutable.

Example

Comment represents a comment posted to a message board. Its implementation follows the Immutable Object pattern. Comment provides the usual getXXX methods. Note that, in this case, a defensive copy is used for the Date field. It also implements the toString, equals, and hashCode methods.

The constructor is responsible for establishing the class invariant, and performs Model Object validation.

package hirondelle.fish.main.discussion; 

import java.util.*;
import hirondelle.web4j.model.ModelCtorException;
import hirondelle.web4j.model.ModelUtil;
import hirondelle.web4j.model.Check;
import hirondelle.web4j.security.SafeText;
import static hirondelle.web4j.util.Consts.FAILS;

/** 
 Comment posted by a possibly-anonymous user.
*/
public final class Comment { 

  /**
   Constructor. 
     
   @param aUserName identifies the logged in user posting the comment. 
   @param aBody the comment, must have content.
   @param aDate date and time when the message was posted.
  */
  public Comment (
    SafeText aUserName, SafeText aBody, Date aDate
  ) throws ModelCtorException {
    fUserName = aUserName;
    fBody = aBody;
    fDate = aDate.getTime();
    validateState();
  }

  /** Return the logged in user name passed to the constructor. */
  public SafeText getUserName() {
    return fUserName;
  }

  /** Return the body of the message passed to the constructor.  */
  public SafeText getBody() {
    return fBody;
  }

  /**
   Return a <a href="http://www.javapractices.com/Topic15.cjp">defensive copy</a> 
   of the date passed to the constructor.
   
   <P>The caller may change the state of the returned value, without affecting 
   the internals of this <tt>Comment</tt>. Such copying is needed since 
   a {@link Date} is a mutable object.
  */
  public Date getDate() {
    // the returned object is independent of fDate
    return new Date(fDate);
  }

  /** Intended for debugging only. */
  @Override public String toString() {
    return ModelUtil.toStringFor(this);
  }

  @Override public boolean equals( Object aThat ) {
    Boolean result = ModelUtil.quickEquals(this, aThat);
    if ( result == null ){
      Comment that = (Comment) 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 SafeText fUserName;
  private final SafeText fBody;
  /** Long is used here instead of Date in order to ensure immutability.*/
  private final long fDate;
  private int fHashCode;
  
  private Object[] getSignificantFields(){
    return new Object[] {fUserName, fBody, new Date(fDate)};
  }
  
  private void validateState() throws ModelCtorException {
    ModelCtorException ex = new ModelCtorException();
    if( FAILS ==  Check.required(fUserName) ) {
      ex.add("User name must have content.");
    }
    if ( FAILS == Check.required(fBody) ) {
      ex.add("Comment body must have content.");
    }
    if ( ! ex.isEmpty() ) throw ex;
  }
}
 

See Also :
Validate state with class invariants
Defensive copying
Immutable objects
Use a testing framework (JUnit)
Data access objects
Avoid JavaBeans style of construction
Use Model View Controller framework
A Web App Framework WEB4J
Validation belongs in a Model Object