Pretend you have a class like this one:
public class CreateUserRequest {
...
@NotNull
@Size(min = 4, max = 16)
@Pattern(regexp = "^[a-zA-Z0-9]+$")
private final String userName;
...
}
What we have here is a field “userName” that has some constraints, so that some values are OK and some are not. You also have a class EditUserRequest. You user userName as user identifier, so you’re going to have this field in EditUserRequest for sure:
public class EditUserRequest {
...
@NotNull
@Size(min = 4, max = 16)
@Pattern(regexp = "^[a-zA-Z0-9]+$")
private final String userName;
...
}
And for sure you’d like to be able to delete users:
public class DeleteUserRequest {
...
@NotNull
@Size(min = 4, max = 16)
@Pattern(regexp = "^[a-zA-Z0-9]+$")
private final String userName;
...
}
All these classes have a field “userName” with absolutely similar meaning and similar constraints. One day you decide that you also want to allow more symbols to be used in userNames. So, you have to manually fix all these 3 classes.
Solution #1
Make a base class for all these requests:
public abstract AbstractUserRequest {
...
@NotNull
@Size(min = 4, max = 16)
@Pattern(regexp = "^[a-zA-Z0-9]+$")
private final String userName;
...
}
public class CreateUserRequest extends AbstractUserRequest {
... // OK, we have userName here
}
Pros:
- You now only have to fix the constraints once
- Creating new types of requests is pretty trivial, you just subclass
AbstractUserRequestand it works.
Cons:
- In case you need
userNamesomewhere else, you’ll have to have a subclass ofAbstractUserRequest(example:CreateUserGroupRequestwhich requires a list ofuserNames, for sure you want to validate them first) - Not excactly sure how to test this properly
Solution #2
Make separate classes for each type of field, so that when you talk about “user name”, you know it’s not just a String, it’s a constrained String:
public class UserNameField {
@NotNull
@Size(min = 4, max = 16)
@Pattern(regexp = "^[a-zA-Z0-9]+$")
private final String value;
...
}
public class CreateUserRequest {
...
private UserNameField userName;
...
}
Pros:
- In case user name constraints change, you’ll only have to fix it once
- You have a simple class which is easy to test
Cons:
- Feels like overkill (?)
Solution #3 (update)
Hibernate validator allows constraint composition, so that you can give a name for a group of constraints:
@NotNull
@Size(min = 4, max = 16)
@Pattern(regexp = "^[a-zA-Z0-9]+$")
@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = {})
@Documented
public @interface UserName {
String message() default "{com.loki2302.constraints.username}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
...
public class CreateUserRequest {
...
@UserName
private final String userName;
...
}
Pros:
- Simple yet powerful approach
- Easy to use (no extra abstractions)
- Easy to test
Cons:
- None probably?
What is the common approach here? Does the Solution #2 look fine? How do you do it?
I would definitely go for Solution #2. As you demonstrated above, you may want to use
UserNameFieldin a class that isn’t a request, which makes theAbstractRequestclass feel like a slightly worse alternative.Creating a class to avoid duplicate code and get all logic in the same place sounds like an excellent idea to me, and not overkill at all.
[EDIT after addition of Solution #3]
Solution #3 looks like a very nice variation of Solution #2. I can’t really decide which one I like more, they both do a fine job of encapsulating the logic.