Parse, Don't Validate

%3aAbBa->bparser.parse(a)

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.

%3cluster_stringStringcluster_passwordPasswordstring_0""string_1"weak"string_2"エイズリエル と 申します‼️"password_3"?Q9|HgU8_asio;<1z%%A@v^"string_n..password_4"vat*#2%!&ExN4F+C$KB-da3"

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.

See Also