Skip to content

A new take on Defensive Programming & Validating Preconditions

License

Notifications You must be signed in to change notification settings

klojang4j/klojang-check

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Klojang Check

Klojang Check is a Java module dedicated to 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. It often seems, however, like everything beyond that is handled 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 needs to be checked nonetheless. Klojang Check allows you to separate precondition validation and business logic in an elegant and concise way.

Klojang Check's 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.

Getting Started

To use Klojang Check, add the following dependency to your Maven POM file:

<dependency>
    <groupId>org.klojang</groupId>
    <artifactId>klojang-check</artifactId>
    <version>3.0.2-jdk21</version>
</dependency>

or Gradle build script:

implementation group: 'org.klojang', name: 'klojang-check', version: '3.0.2-jdk21'

Example

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 ...
  }

}

Documentation

The Javadocs for Klojang Check can be found here.

The User Guide for Klojang Check can be found here.

The latest test coverage results are here.

Vulnerabilities

Being all about making code as robust as possible, the Klojang Check code base is itself regularly tested for vulnerabilities. It is currently not affected by any CVE, however light-weight. Its surface consists of barely 15 types and it has zero dependencies outside java.base. You can find the latest vulnerabilities report here.

Performance

No one is going to use a library just to check things that aren't even related to their business logic if it hogs 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's no two ways around it. Klojang Check just lets you express this fact more clearly:

Check.that(value).is(keyIn(), map);

The latest JMH benchmarks can be found here.

Usage

Null checks

// throws a NullPointerException if foo is null
Check.notNull(foo);

The CommonChecks class

The CommonChecks class is a grab bag of common checks on arguments, fields (a.k.a. state) and other types of program input.

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);

Providing an Argument Name

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)

You can provide a name for the value you are testing to give the user more context:

Check.that(length, "length").is(gte(), 0);
// error message: length must be >= 0 (was -42)

The Tag class contains string constants for some commonly used argument names:

import static org.klojang.check.Tag.LENGTH;

Check.that(length, LENGTH).is(gte(), 0);
// error message: length must be >= 0 (was -42)

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 Function, ToIntFunction and IntFunction constants that might be of help:

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

If you prefer, you can provide your own error message:

Check.that(foo).is(notNull(), "there you go again");
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 ${0} characters (was ${1})",
      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})");

${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 because 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:

  1. by providing a Function that takes a string (the error message) and returns the exception to be thrown;
  2. by providing a Supplier that supplies the exception to be thrown.

Here is an example of each of these:

// 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 the 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 logical 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"));

About

Klojang Check is developed by Naturalis, the Dutch national biodiversity research institute. It maintains one of the largest collections of zoological and botanical specimens in the world. Help fund biodiversity research by donating to Naturalis.

About

A new take on Defensive Programming & Validating Preconditions

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published