Immutable objects
Use a testing framework (JUnit)
Verify input with regular expressions
Input dialogs
Validation belongs in a Model Object
Implementing validation in the Model Object has some strong advantages:
Example
Here's 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.
/** Data-centric class encapsulating all fields related to movies (a 'model object'). <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. */ public final class Movie implements Comparable<Movie>{ /** Constructor taking regular Java objects natural to the domain. @param aId optional, the database identifier 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; } String getTitle(){ return fTitle; } Date getDateViewed(){ return fDateViewed; } BigDecimal getRating(){ return fRating; } String getComment(){ return fComment; } //elided... // 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; //elided... 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; } } }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. */ @Override 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 saved", JOptionPane.ERROR_MESSAGE ); } }