Motivation;

I want to report on all the possible problems at once instead of having to peel the onion of every possible problem over and over. It also makes testing much easier because I can have one test for success and one test that handles any/all failures. Much like you do form validation on the client side before you submit to the back end. But instead the back end is letting clients out of its control know what is wrong in total instead of peeling the onion one validation failure at a time.

I am trying to come up with a pattern to test for multiple preconditions that all must be true and more than one may be false.

Example:

Not to be taken literally as the problem to solve, I am just showing how the Guava stuff balloons out. It could be a bunch of if/elseif/then/else statements, or a bunch of Predicates that are all chained together with .and(). This is just a strawman to show how the scope balloons out of control.

public boolean process(@Nonnull final String left, @Nonnull final String right)
{
    Preconditions.checkArgument(!left.isEmpty());
    Preconditions.checkArgument(!right.isEmpty());
    Preconditions.checkArgument(/* left meets some regex */)
    Preconditions.checkArgument(/* right meets some regex */)
    /* more, more, more, etc. */
    /* actual logic goes here */
}

I could obviously wrap each individual check with a try/catch block build a List<IllegalArgumentException> test if that !isEmpty() and then build a ChainedException with all the root causes and throw it but that is a lot of boilerplate code to put in every method that has multiple Precondition requirements.java

Now imagine that I have a lot more preconditions than this, and it is in a Builder.build() method. The client side form validation idiom makes good sense in these cases, like in the case of a Microservice that takes a payload and validates it before it acts on it. If it has dozens of preconditions it gets ugly fast. You do not want to have to make N calls with error codes for N validation failures, you want to send back all the problems in one error response instead of possibly dozens of back and forth.

public boolean save(@Nonnull final SomeDomainObjectWithLotsOfFields sdowlof) 
{ /* lots and lots of boilerplate to handle all the preconditions */ }

I know there has to be some kind of ChainablePreconditions pattern that I just can not find and wanted to make sure I am not missing something before I re-invent the wheel and write my on ChainOfResponsibliy implemenation.

If not I will post an answer with a link to GitHub for whatever I come up with as an alternative.

There are two very different kinds of errors:

  • errors on the level of the problem domain you are working on, e.g. form validation errors, and
  • logic errors within your program, e.g. null pointer exceptions.

Both may be communicated through exceptions but they are fundamentally different:

  • Problem domain errors should be handled so that your program can continue. If these errors are not detected and handled your program is wrong, and possibly underspecified.
  • Logic errors are not recoverable: your program is corrupted and there is no point in continuing. In some cases a complete crash is unacceptable so the application will merely restart itself. Your program is broken whether or not you check for these errors, but detecting them early is usually preferable to continuing to run with silent corruption.

Guava Preconditions are intended as a kind of assertion that guards against logic errors. If they fail there's no point in continuing and they immediately raise an IllegalArgumentException or other exceptions as appropriate. The program should still be correct if these assertions were removed.

What you gain by throwing a single exception that aggregates all precondition failures is not any change in correctness, but only a better developer experience. This may be a worthy goal! Especially if you are creating a library that will be used by many developers, great error messages are quite valuable. In application code, the value is more dubious. You'll still get the error messages either way, so there won't be much debugging time saved. All developers are likely sufficiently familiar with the code base to know where to look when there are problems.

If you really do want to use these grouped assertion messages, you will have to write your own assertion functions for that. You mention a Chain Of Responsibility pattern, which is one possible approach. However, it might be awkward to configure.

One approach I would suggest if you can use Java 8 is to create a function that takes multiple validation callbacks, and takes care of catching the exceptions as necessary. Roughly:

@FunctionalInterface Precondition { void check(); }
static void checkAll(Precondition... preconditions) {
  List<Throwable> errors = new ArrayList<>();
  for (Precondition p : preconditions) {
    try { p.check(); }
    catch (IllegalArgumentException | NullPointerException | ... e) {
      errors.add(e);
    }
  }
  throw ...;
}

Called as

checkAll(
  () -> Preconditions.checkNotNull(a),
  () -> Preconditions.checkNotNull(b, "b must be present"),

However, that extra machinery might have a prohibitive runtime overhead. Preconditions should be as cheap as possible, and introducing lambdas won't help here.

This also doesn't generalize well if there are dependencies between the precondition checks. Consider for example code like:

Preconditions.checkNotNull(a);
Preconditions.checkNotNull(map);
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(map.get(key));

All in all, do build yourself nice abstractions if they help you, but pouring too much effort into a special case of your developer experience only provides very limited value.

  • I have a similar hacked together solution using Predicate instances that are .and() together and it gets too verbose without some kind of fluent api or some annotation magic to hide the boilerplate. I am not really worried about the overhead because I have been doing stuff like this with JsonSchema and it does not get much more overhead than that. – Jarrod Roberson Jul 15 at 21:51

Your Answer

 
discard

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.