Unit testing and integration testing have long since become established practices. Practically all Java programmers know how to write unit tests with JUnit. IDEs will help you with it and build tools like Maven and Gradle and run them as a matter of course.
The same cannot be said of its (sort of) runtime counterpart: Defensive Programming — ensuring your program or method starts with a clean and workable set of inputs before continuing with the business logic. Null checks are the most common example of this. Yet it often seems like everything beyond that is treated as part of the business logic, even when it arguably isn’t. If a method that calculates a price needs some value from a configuration file, is the presence of the configuration file part of the business logic? Probably not, but it should be checked nonetheless.
This is where Klojang Check steps in. Klojang Check is a small Java library that enables you to separate precondition validation and business logic in a clean, elegant, and concise manner. Its take on precondition validation is rather different from, for example, Guava’s Preconditions class or Apache’s Validate class. It provides a set of syntactical constructs that make it easy to specify checks on program input, object state, method arguments, variables, etc. In addition, it comes with a set of common checks on values of various types. These checks are associated with short, informative error messages, so you don’t have to invent them yourselves.
Here is an example of Klojang Check in action:
public class InteriorDesigner { private final int numChairs; public InteriorDesigner(int numChairs) { this.numChairs = Check.that(numChairs) .is(gt(), 0) .is(lte(), 4) .is(even()) .ok(); } public void applyColors(List<Color> colors) { Check.that(colors).is(notEmpty().and(contains(), noneOf(), RED, BLUE, PINK)); // apply the colors ... } public void addCouch(Couch couch) { Check.that(couch).isNot(Couch::isExpensive, ExpensiveCouchException::new); // add the couch ... } }
Performance
No one is going to use a library just to check things that aren’t even related to their business logic if it is going to hog their CPU. Klojang Check incurs practically zero overhead. That’s because it doesn’t really do stuff. As mentioned, it only provides a set of syntactical constructs that make precondition validation more concise. Of course, if a value needs to be in a Map before it even makes sense to continue with the rest of a computation, you will have to do the lookup. There are no two ways around it. Klojang Check just lets you express this fact more clearly:
Check.that(value).is(keyIn(), map);
If you are interested, you can find the results of the JMH benchmarks here.
Getting Started
To start using Klojang Check, add the following dependency to your POM file:
Common Checks
The CommonChecks class is a grab bag of common checks on arguments, fields (a.k.a. state), and other types of program input. Here are some examples:
import static org.klojang.check.CommonChecks.*; Check.that(length).is(gte(), 0);
Check.that(divisor).isNot(zero());
Check.that(file).is(writable());
Check.that(firstName).is(substringOf(), fullName);
Check.that(i).is(indexOf(), list);
Think of all the if statements it would have taken to hand-code these checks!
Testing Argument Properties
With Klojang Check, you can test not just arguments but also argument properties.
To do this, provide a Function that extracts the value to be tested from the argument.
Check.that(fullName).has(String::length, lte(), 100);
The CommonProperties class contains some useful functions that can make your life easier:
import static org.klojang.check.CommonProperties.strlen;
import static org.klojang.check.CommonProperties.type;
import static org.klojang.check.CommonProperties.abs; Check.that(fullName).has(strlen(), lte(), 100);
Check.that(foo).has(type(), instanceOf(), InputStream.class);
Check.that(angle).has(abs(), lte(), 90);
As the last example illustrates, the word “property” needs to be taken in the broadest sense here. These are really just functions that are passed the argument and return the value to be tested.
Providing A Custom Error Message
Klojang Check generates a short, informative error message if the input value fails
a test:
Check.that(length).is(gte(), 0);
// error message: argument must be >= 0 (was -42)
But you can provide your own error message if you prefer:
Check.that(fullName).has(strlen(), lte(), 100, "full name must not exceed 100 characters");
The message may itself contain message arguments:
Check.that(fullName).has(strlen(), lte(), maxLength, "full name must not exceed Check.that(fullName).has(strlen(), lte(), maxLength, "full name must not exceed ${0} characters (was ${1})", maxLength
fullName.length());
characters (was )", maxLength
fullName.length());
There are a few predefined message arguments that you can use in your error message:
Check.that(fullName).has(strlen(), lte(), maxLength, "full name must not exceed ${obj} characters (was ${arg})");
This code snippet is exactly equivalent to the previous one, but this time you didn’t have to provide any message arguments yourself! ${arg}
is the value you are testing, while ${obj}
is the value you are testing it against. The reason the latter message argument is called ${obj} is that it is the object of the less-than-or-equal-to relationship, while the argument is used as the subject of that relationship. (For more information, see here.)
Throwing A Custom Exception
By default, Klojang Check will throw an IllegalArgumentException
if the input
value fails any of the checks following Check.that(...)
. This can be customized in two ways:
- By providing a function that takes a string (the error message) and returns the exception to be thrown;
- By providing a supplier that supplies the exception to be thrown.
Here is an example for each of the two options:
// Error message "stale connection" is passed to the constructor of IllegalStateException:
Check.on(IllegalStateException::new, connection.isOpen()).is(yes(), "stale connection");
Check.that(connection.isOpen()).is(yes(), () -> new IllegalStateException("stale connection"));
The CommonExceptions class contains exception factories for some common exceptions:
import static org.klojang.check.CommonExceptions.STATE;
import static org.klojang.check.CommonExceptions.illegalState; Check.on(STATE, connection.isOpen()).is(yes(), "stale connection");
Check.that(connection.isOpen()).is(yes(), illegalState("stale connection"));
Combining Checks
Sometimes you will want to do tests of form x must be either A or B or of the form either x must be A or y must be B:
Check.that(collection).is(empty().or(contains(), "FOO"));
Check.that(collection1).is(empty().or(collection2, contains(), "FOO"));
The latter example nicely maintains the Klojang Check idiom, but if you prefer your code with less syntactical sugar, you can also just write:
Check.that(collection1).is(empty().or(collection2.contains("FOO"));
When combining checks, you can also employ quantifiers:
import static org.klojang.check.relation.Quantifier.noneOf;
import static org.klojang.check.CommonChecks.notEmpty;
import static org.klojang.check.CommonChecks.contains; Check.that(collection).is(notEmpty().and(contains(), noneOf(), "FOO", "BAR");
Conclusion
We hope this article has given you a flavor of how you can use Klojang Check to much more systematically separate precondition validation from business logic. Klojang Check‘s conciseness hopefully lowers the bar significantly to just list everything that should be the case for your program or method to continue normally — and write a check for it!