Parse, Don't Validate
Once you validate raw input, turn it into another type as proof that it is validated. The type safety stops the possibility of invalid values propagating far into the code.
Parser:
-
Function that transforms an input type to a target type, iff it matches certain criteria.
-
The target type's constructor must be restricted to the parser.
Ensures the target type can never be constructed without going through the parser.
-
Raise an error if the criteria is not met.
Example: Updating a password only if the proposed value is strong.
Switch from:
// Usage
void resetPassword(final String passwordProposed) throws PasswordTooWeakException {
passwordChecker.check(passwordProposed);
// password is strong
passwordUpdater.update(profile, passwordProposed);
}
// our "validator"
public class PasswordChecker {
public void check(String passwordProposed) throws PasswordTooWeakException {
if (isStrong(passwordProposed)) {
// ok!
return;
} else {
throw new PasswordTooWeakException(passwordProposed);
}
}
}
// our provider
public class PasswordUpdater {
public void update(Profile profile, String newPassword) { /* .. */ }
}
to:
// Usage
void resetPassword(final String passwordProposed) throws PasswordTooWeakException {
Password password = passwordChecker.check(passwordProposed);
passwordUpdater.update(profile, password);
}
public class Password {
private String value;
// package private visibility
Password(String value) { /* .. */ }
// ..
}
// our "parser"
public class PasswordChecker {
public Password check(String passwordProposed) throws PasswordTooWeakException {
if (isStrong(passwordProposed)) {
return new Password(passwordProposed);
} else {
throw new PasswordTooWeakException(passwordProposed);
}
}
}
// our provider
public class PasswordUpdater {
public void update(Profile profile, Password newPassword) { /* .. */ }
}
Example: Only accepting a list of one-or-more elements.
Switch from:
void displaySuggestions(final List<Suggestion> suggestions) {
// ..
}
to:
void displaySuggestions(final ListMinOneElement<Suggestion> suggestions) {
// ..
}
public <T> class ListMinOneElement<T> {
private List<T> inner;
private ListMinOneElement(List<T> inner) {
this.inner = inner;
}
public static ListMinOneElement<T> tryFrom(List<T> maybeEmptyList)
throws ListEmptyException {
if (maybeEmptyList.isEmpty()) {
throw new ListEmptyException(maybeEmptyList);
} else {
return new ListMinOneElement(maybeEmptyList);
}
}
}
Bug Variants Addressed
-
1: Raises an error1 at the point of parsing.
-
2: Reduces provider code working with invalid input, because only valid input reaches the business logic.
-
3: Well-named restrictive types informs the consumer of constraints the input must adhere to, and parsing enforces the constraints.
-
6: Constrains input to a range of safe values.
If we have a list of safe commands that input is parsed into, then we protect ourselves from running unsafe logic.
1 It is not a bug to raise an error.