Иногда лучше использовать строгую типизацию вместо повторения одинаковых проверок для всех слоев и уровней. Интересно то, что сделать класс устойчивым к неправильному использованию очень похоже на использование 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/