Verify input with Model Objects
Validation of user input is important for almost all applications. There seems to be two places where you may implement such validation logic
- directly in the view-related classes
- not in the view, but in your Model Object instead
Implementing validation in the Model Object has some strong advantages :
- the Model Object is the natural home for such logic
- since the code is not tied to the GUI, it is much simpler to test using tools such as JUnit
- complex validations that depend on more than one input item are handled just as easily as any other validation
Example
Here is a Model Object class named Movie.
It performs all its validation in its constructors.
If a problem occurs, a checked exception named InvalidUserInput is thrown.
package hirondelle.movies.edit; import java.math.BigDecimal; import java.util.*; import hirondelle.movies.exception.InvalidInputException; import hirondelle.movies.util.Util; /** Data-centric class encapsulating all fields related to movies. <P>This class exists in order to encapsulate, validate, and sort movie information. This class is used both to validate user input, and act as a 'transfer object' when interacting with the database. <P><b>This class would greatly benefit from a JUnit test class, to test its data validation and sorting.</b> */ final class Movie implements Comparable<Movie>{ /** Constructor taking regular Java objects natural to the domain. <P>When the user has entered text, this constructor is called indirectly, through {@link #Movie(String, String, String, String, String)}. @param aId optional, the database identifer for the movie. This item is optional since, for 'add' operations, it has yet to be assigned by the database. @param aTitle has content, name of the movie @param aDateViewed optional, date the movie was screened by the user @param aRating optional, in range 0.0 to 10.0 @param aComment optional, any comment on the movie */ Movie( String aId, String aTitle, Date aDateViewed, BigDecimal aRating, String aComment ) throws InvalidInputException { fId = aId; fTitle = aTitle; fDateViewed = aDateViewed; fRating = aRating; fComment = aComment; validateState(); } /** Constructor which takes all parameters as <em>text</em>. <P>Raw user input is usually in the form of <em>text</em>. This constructor <em>first</em> parses such text into the required 'base objects' - {@link Date}, {@link BigDecimal} and so on. If those parse operations <em>fail</em>, then the user is shown an error message. If N such errors are present in user input, then N <em>separate</em> message will be presented for each failure, one by one. <P>If all such parse operations <em>succeed</em>, then the "regular" constructor {@link #Movie(String, String, Date, BigDecimal, String)} will then be called. It's important to note that this call to the second constructor can in turn result in <em>another</em> error message being shown to the user (just one this time). */ Movie( String aId, String aTitle, String aDateViewed, String aRating, String aComment ) throws InvalidInputException { this( aId, aTitle, Util.parseDate(aDateViewed, "Date Viewed"), Util.parseBigDecimal(aRating, "Rating"), aComment ); } String getId(){ return fId; } /** This set method is rather artificial. It results from the toy persistence layer. It's dissatisfying to add this method since the class would otherwise be immutable. Immutability is a highly desirable characteristic. */ void setId(String aId){ fId = aId; } String getTitle(){ return fTitle; } Date getDateViewed(){ return fDateViewed; } BigDecimal getRating(){ return fRating; } String getComment(){ return fComment; } @Override public boolean equals(Object aThat){ if ( this == aThat ) return true; if ( !(aThat instanceof Movie) ) return false; Movie that = (Movie)aThat; return areEqual(this.fTitle, that.fTitle) && areEqual(this.fDateViewed, that.fDateViewed) && areEqual(this.fRating, that.fRating) && areEqual(this.fComment, that.fComment) ; } @Override public int hashCode(){ int result = 17; result = addHash(result, fTitle); result = addHash(result, fDateViewed); result = addHash(result, fRating); result = addHash(result, fComment); return result; } @Override public String toString(){ return "Movie Id:" + fId + " Title:" + fTitle + " Date Viewed:" + fDateViewed + " Rating:" + fRating + " Comment: " + fComment ; } /** Default sort by Date Viewed, then Title. Dates have the most recent items listed first. */ public int compareTo(Movie aThat) { if ( this == aThat ) return EQUAL; int comparison = DESCENDING*comparePossiblyNull(this.fDateViewed, aThat.fDateViewed); if ( comparison != EQUAL ) return comparison; comparison = this.fTitle.compareTo(aThat.fTitle); if ( comparison != EQUAL ) return comparison; comparison = comparePossiblyNull(this.fRating, aThat.fRating); if ( comparison != EQUAL ) return comparison; comparison = comparePossiblyNull(this.fComment, aThat.fComment); if ( comparison != EQUAL ) return comparison; return EQUAL; } /** Sort by Title. */ static Comparator<Movie> TITLE_SORT = new Comparator<Movie>(){ public int compare(Movie aThis, Movie aThat) { if ( aThis == aThat ) return EQUAL; int comparison = aThis.fTitle.compareTo(aThat.fTitle); if ( comparison != EQUAL ) return comparison; comparison = DESCENDING*comparePossiblyNull(aThis.fDateViewed, aThat.fDateViewed); if ( comparison != EQUAL ) return comparison; comparison = comparePossiblyNull(aThis.fRating, aThat.fRating); if ( comparison != EQUAL ) return comparison; comparison = comparePossiblyNull(aThis.fComment, aThat.fComment); if ( comparison != EQUAL ) return comparison; return EQUAL; }; }; /** Sort by Rating (descending), then Date Viewed (descending). */ static Comparator<Movie> RATING_SORT = new Comparator<Movie>(){ public int compare(Movie aThis, Movie aThat) { if ( aThis == aThat ) return EQUAL; int comparison = DESCENDING*comparePossiblyNull(aThis.fRating, aThat.fRating); if ( comparison != EQUAL ) return comparison; comparison = DESCENDING*comparePossiblyNull(aThis.fDateViewed, aThat.fDateViewed); if ( comparison != EQUAL ) return comparison; comparison = aThis.fTitle.compareTo(aThat.fTitle); if ( comparison != EQUAL ) return comparison; comparison = comparePossiblyNull(aThis.fComment, aThat.fComment); if ( comparison != EQUAL ) return comparison; return EQUAL; }; }; /** Sort by Comment. */ static Comparator<Movie> COMMENT_SORT = new Comparator<Movie>(){ public int compare(Movie aThis, Movie aThat) { if ( aThis == aThat ) return EQUAL; int comparison = comparePossiblyNull(aThis.fComment, aThat.fComment); if ( comparison != EQUAL ) return comparison; comparison = aThis.fTitle.compareTo(aThat.fTitle); if ( comparison != EQUAL ) return comparison; comparison = comparePossiblyNull(aThis.fRating, aThat.fRating); if ( comparison != EQUAL ) return comparison; comparison = DESCENDING*comparePossiblyNull(aThis.fDateViewed, aThat.fDateViewed); if ( comparison != EQUAL ) return comparison; return EQUAL; }; }; // PRIVATE // private String fId; private final String fTitle; private final Date fDateViewed; private final BigDecimal fRating; private final String fComment; private static final BigDecimal TEN = new BigDecimal("10.0"); private static final int EQUAL = 0; private static final int DESCENDING = -1; private void validateState() throws InvalidInputException { InvalidInputException ex = new InvalidInputException(); if( ! Util.textHasContent(fTitle) ) { ex.add("Title must have content"); } if ( fRating != null ){ if ( fRating.compareTo(BigDecimal.ZERO) < 0 ) { ex.add("Rating cannot be less than 0."); } if ( fRating.compareTo(TEN) > 0 ) { ex.add("Rating cannot be greater than 10."); } } if ( ex.hasErrors() ) { throw ex; } } private boolean areEqual(Object aThis, Object aThat){ return aThis == null ? aThat == null : aThis.equals(aThat); } private int addHash(int aHash, Object aField){ int result = 37*aHash; if (aField != null){ result = result + aField.hashCode(); } return result; } /** Utility method. */ private static <T extends Comparable<T>> int comparePossiblyNull(T aThis, T aThat){ int result = EQUAL; int BEFORE = -1; int AFTER = 1; if(aThis != null && aThat != null){ result = aThis.compareTo(aThat); } else { //at least one reference is null - special handling if(aThis == null && aThat == null) { //do nothing - they are not distinct } else if(aThis == null && aThat != null) { result = BEFORE; } else if( aThis != null && aThat == null) { result = AFTER; } } return result; } }
The user edits Movie data using a dialog (not listed here). When the user hits a button, execution passes to the following MovieController class. The actionPerformed method first attempts to build a Movie object from user input. If a problem is detected, then an error message is displayed to the user.
package hirondelle.movies.edit; import hirondelle.movies.exception.InvalidInputException; import hirondelle.movies.main.MainWindow; import hirondelle.movies.util.Edit; import hirondelle.movies.util.Util; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.logging.Logger; import javax.swing.JOptionPane; /** Add a new {@link Movie} to the database, or change an existing one. <P>It's important to note that this class uses most of the other classes in this feature to get its job done (it doesn't use the <tt>Action</tt> classes): <ul> <li>it gets user input from the view - {@link MovieView} <li>it validates user input using the model - {@link Movie} <li>it persists the data using the Data Access Object - {@link MovieDAO} </ul> */ final class MovieController implements ActionListener { /** Constructor. @param aView user interface @param aEdit identifies what type of edit - add or change */ MovieController(MovieView aView, Edit aEdit){ fView = aView; fEdit = aEdit; } /** Attempt to add a new {@link Movie}, or edit an existing one. <P>If the input is invalid, then inform the user of the problem(s). If the input is valid, then add or change the <tt>Movie</tt>, close the dialog, and update the main window's display. */ public void actionPerformed(ActionEvent aEvent){ fLogger.fine("Editing movie" + fView.getTitle()); try { createValidMovieFromUserInput(); } catch(InvalidInputException ex){ informUserOfProblems(ex); } if ( isUserInputValid() ){ if( Edit.ADD == fEdit ) { fLogger.fine("Add operation."); fDAO.add(fMovie); } else if (Edit.CHANGE == fEdit) { fLogger.fine("Change operation."); fDAO.change(fMovie); } else { throw new AssertionError(); } fView.closeDialog(); MainWindow.getInstance().refreshView(); } } // PRIVATE // private final MovieView fView; private Movie fMovie; private Edit fEdit; private MovieDAO fDAO = new MovieDAO(); private static final Logger fLogger = Util.getLogger(MovieController.class); private void createValidMovieFromUserInput() throws InvalidInputException { fMovie = new Movie( fView.getId(), fView.getTitle(), fView.getDateViewed(), fView.getRating(), fView.getComment() ); } private boolean isUserInputValid(){ return fMovie != null; } private void informUserOfProblems(InvalidInputException aException) { Object[] messages = aException.getErrorMessages().toArray(); JOptionPane.showMessageDialog( fView.getDialog(), messages, "Movie cannot be added", JOptionPane.ERROR_MESSAGE ); } }
See Also :
Immutable objects
Use a testing framework (JUnit)
Verify input with regular expressions
Input dialogs
Validation belongs in a Model Object
Use a testing framework (JUnit)
Verify input with regular expressions
Input dialogs
Validation belongs in a Model Object
Would you use this technique?
|
|