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}; } }