Sometimes, it’s nice to use strong typing instead of repeating the same checks all over the layers and tiers. The interesting thing is that making a class robust against misuse is very similar to using Java Bean Validation.
A classical approach may look like this:
public class User {
private static final Pattern PATTERN = Pattern.compile("[a-z][0-9a-z_\\-]*");
private String name;
public User(String name) {
super();
if (name == null) {
throw new IllegalArgumentException("name == null");
}
String trimmed = name.trim().toLowerCase();
if (trimmed.length() == 0) {
throw new IllegalArgumentException("length name == 0");
}
if (trimmed.length() < 3) {
throw new IllegalArgumentException("length name < 3");
}
if (trimmed.length() > 20) {
throw new IllegalArgumentException("length name > 20");
}
if (!PATTERN.matcher(trimmed).matches()) {
throw new IllegalArgumentException("name pattern violated");
}
this.name = trimmed;
}
}
Using Bean Validation, we could create a custom constraint instead:
@Size(min = 3, max = 20)
@Pattern(regexp = "[a-z][0-9a-z_\\-]*")
@Target({ ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
@Documented
public @interface UserName {
String message() default "{org.fuin.blog.UserName.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
The User class now looks much better:
public class User {
@NotNull
@UserName
private String name;
public User(String name) {
super();
this.name = name;
}
}
But now, the object has lost the ability to protect itself against misuse. It’s no longer a robust object. Maybe someone uses a validator to check if the object is valid; maybe not. In any case, it’s always possible to create invalid objects of this kind.
How about combining both techniques?
Let’s rename the UserName annotation into UserNameStr because it actually works on a string, and this way, we can also avoid a name clash with a new strong type we will create soon:
@Size(min = 3, max = 20)
@Pattern(regexp = "[a-z][0-9a-z_\\-]*")
@Target({ ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
@Documented
public @interface UserNameStr {
String message() default "{org.fuin.blog.UserNameStr.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Next, we create a base type for all such String based on strong typing:
public abstract class AbstractStringBasedType<T extends AbstractStringBasedType<T>> implements Comparable<T>, Serializable {
private static final long serialVersionUID = 0L;
private static final Validator VALIDATOR;
static {
VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
}
public final int hashCode() {
return nullSafeToString().hashCode();
}
public final boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final T other = (T) obj;
return nullSafeToString().equals(other.nullSafeToString());
}
public final int compareTo(final T other) {
return this.nullSafeToString().compareTo(other.nullSafeToString());
}
public final int length() {
return nullSafeToString().length();
}
protected final void requireValid(final T value) {
final Set<ConstraintViolation<T>> constraintViolations = VALIDATOR.validate(value);
if (constraintViolations.size() > 0) {
final StringBuffer sb = new StringBuffer();
for (final ConstraintViolation<T> constraintViolation : constraintViolations) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append("[" + constraintViolation.getPropertyPath() + "] "
+ constraintViolation.getMessage() + " {"
+ constraintViolation.getInvalidValue() + "}");
}
throw new IllegalArgumentException(sb.toString());
}
}
private String nullSafeToString() {
final String str = toString();
if (str == null) {
return "null";
}
return str;
}
public abstract String toString();
}
The refactored UserName class now uses the Bean Validation API to perform a constraint check at the end of the constructor, which means one is no longer able to create invalid objects:
public final class UserName extends AbstractStringBasedType<UserName> {
private static final long serialVersionUID = 0L;
@NotNull
@UserNameStr
private final String userName;
public UserName(final String userName) {
super();
this.userName = userName;
// Always the last line in the constructor!
requireValid(this);
}
public String toString() {
return userName;
}
}
The refactored User class is now even simpler and contains only a @NotNull annotation on the name property:
public class User {
// Only null check here, because all other
// checks are done by user name itself
@NotNull
private UserName name;
public User(UserName name) {
super();
this.name = name;
}
}
Here is a simple example using the UserName type:
public class Example {
public static void main(String[] args) {
Locale.setDefault(Locale.ENGLISH);
try {
new UserName(null);
} catch (IllegalArgumentException ex) {
// [userName] may not be null {null}
}
try {
new UserName("");
} catch (IllegalArgumentException ex) {
// [userName] must match "[a-z][0-9a-z_\-]*" {},
// [userName] size must be between 3 and 20 {}
}
try {
new UserName("_a1");
} catch (IllegalArgumentException ex) {
// [userName] must match "[a-z][0-9a-z_\-]*" {_a1}
}
// Valid name
System.out.println(new UserName("john-2_a"));
}
}
If we don’t want to use the strong typing, it’s easy to use only annotations. This ability is helpful especially where you have to deal with non-Java clients. In these situations, a DTO may contain only a simple annotated String:
public class UserDTO implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull
@UserNameStr
private String name;
public UserDTO(String name) {
super();
this.name = name;
}
}
Now strong types and Bean Validation can live in peaceful coexistence within your application. It’s a good idea to check if your GUI controls support such JS303 enhanced strong types before you start using it. Otherwise, you may lose the ability to validate upfront on the client (e.g. checking input length based on the @Size annotation).