Иногда лучше использовать строгую типизацию вместо повторения одинаковых проверок для всех слоев и уровней. Интересно то, что сделать класс устойчивым к неправильному использованию очень похоже на использование Java Bean Validation.
Классический подход может выглядеть так:
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;
}
}
Используя Bean Validation, мы могли бы вместо этого создать собственное ограничение:
@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 {};
}
Класс User теперь выглядит намного лучше:
public class User {
@NotNull
@UserName
private String name;
public User(String name) {
super();
this.name = name;
}
}
Но теперь объект утратил способность защищать себя от неправильного использования. Это больше не надежный объект. Может быть, кто-то использует валидатор, чтобы проверить, является ли объект действительным; возможно, нет. В любом случае всегда можно создать недопустимые объекты такого рода.
Как насчет сочетания обеих техник?
Давайте переименуем аннотацию UserName в UserNameStr, потому что она фактически работает со строкой, и таким образом, мы также можем избежать столкновения имен с новым сильным типом, который мы скоро создадим:
@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 {};
}
Далее мы создаем базовый тип для всех таких строк на основе строгой типизации:
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();
}
Реорганизованный класс UserName теперь использует API Bean Validation для выполнения проверки ограничений в конце конструктора, что означает, что больше нельзя создавать недопустимые объекты:
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;
}
}
Измененный класс User теперь стал еще проще и содержит только аннотацию @NotNull для свойства name:
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;
}
}
Вот простой пример использования типа UserName:
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"));
}
}
Если мы не хотим использовать строгую типизацию, легко использовать только аннотации. Эта возможность особенно полезна, когда вам приходится иметь дело с не-Java клиентами. В этих ситуациях DTO может содержать только простую аннотированную строку:
public class UserDTO implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull
@UserNameStr
private String name;
public UserDTO(String name) {
super();
this.name = name;
}
}
Теперь сильные типы и проверка бобов могут жить в мирном сосуществовании в вашем приложении. Перед началом использования рекомендуется проверить, поддерживают ли элементы управления графического интерфейса такие расширенные типы JS303. В противном случае вы можете потерять возможность предварительной проверки на клиенте (например, проверка длины ввода на основе аннотации @Size).
От http://javadeveloperslife.wordpress.com/2011/12/05/combining-strong-typing-and-bean-validation-jsr-303/