Which design pattern to use to avoid if/else in validation classes?

Viewed 596

I am currently using HibernateConstraintValidator to implement my validations. But my reviewer is not fine with having if/else in code or ! operators. Which design pattern can I use to remove the if/else in my validation logic?

public class SomeValidatorX implements ConstraintValidator<SomeAnnotation, UUID> {

      @Autowired
      SomeRepository someRepository;
   
      @Override
      public boolean isValid(UUID uuid, ConstraintValidationContext context) {

             return !(uuid!=null && someRepository.existsById(uuid)); //The reviewer doesn't want this negation operator
      }
}

And in below code, he doesn't want if/else

public class SomeValidatorY implements ConstraintValidator<SomeAnnotation, SomeClass> {

      @Autowired
      SomeRepository someRepository;
   
      @Override
      public boolean isValid(SomeClass someObject, ConstraintValidationContext context) {
           if(someObject.getFieldA() != null) { //He doesn't want this if statement
                //do some operations
                List<Something> someList = someRepository.findByAAndB(someObject.getFieldA(),B);
                return !someList.isEmpty(); //He doesn't want this ! operator
           }
           return false; // He was not fine with else statement in here as well
      }
}

Side Note: We have to use Domain Driven Design (if it helps)

2 Answers

A long time ago, in the beginning of time. There was a guideline that said that methods should only have one exit point. To achieve that, developers had to track the local state and use if/else to be able to reach the end of the method.

Today we know better. By exiting a method as early as possible it's much easier to keep the entire flow in our head while reading the code. Easier code means less mistakes. Less mistakes equals less bugs.

In my opinion, that's why the reviewer doesn't like the code. It's not as easy to read as it could be.

Let's take the first example:

 public boolean isValid(UUID uuid, ConstraintValidationContext context) {

         return !(uuid!=null && someRepository.existsById(uuid)); //The reviewer doesn't want this negation operator
  }

What the code says is "not this: (uuid should not be empty and it must exist)". Is that easy to understand? I think not.

The alternative: "Its OK if uuid do not exist, but if it do, the item may not exist".

Or in code:

if (uuid == null) return true;
return !someRepository.existsById(uuid);

Much easier to read, right? (I hope that I got the intention correct ;))

Second example

      if(someObject.getFieldA() != null) { //He doesn't want this if statement
            //do some operations
            List<Something> someList = someRepository.findByAAndB(someObject.getFieldA(),B);
            return !someList.isEmpty(); //He doesn't want this ! operator
       }
       return false; // He was not fine with else statement in here as well

Ok. Here you are saying:

  • If field A is not null:
    • Build a list where A and b is found
    • If that list is not empty fail, otherwise succeed.
  • Otherwise fail

A easier way to conclude that is to simply say:

  • It's ok if field A is not specified
  • If field A is specified it must exist in combination with B.

Translated to code:

if (someObject.getFieldA() == null) 
    return true;

return !someRepository.findByAAndB(someObject.getFieldA(),B).isEmpty();

In C# we have Any() which is opposite to isEmpty which I would prefer in this case as it removes the negation.

Sometimes negations are required. It doesn't make sense to write a new method in the repository to avoid it. However, if findByAAndB is only used by this I would rename it to ensureCombination(a,b) so that it can return true for the valid case.

Try to write code as you talk, it makes it much easier to create a mental picture of the code then. You aren't saying "Im not full, lets go to lunch", are you? ;)

You can check the Null-object pattern. The general pattern is to ban null completely from your code. This eliminates the ugly null checks. In this point I agree with your code reviewer.

Following the below recommendations will result in:

public boolean isValid(SomeClass someObject, ConstraintValidationContext context) {
  return someRepository.containsAAndB(someObject.getFieldA(), B);
}

Avoid null checks

Before introducing the Null-object pattern, simply apply the pattern or convention to enforce initialization of all references. This way you can be sure that there are no null references in your entire code.
So when you encounter a NullPointerException, you don't solve the issue by introducing a null check, but by initializing the reference (on construction) e.g., by using default values, empty collections or null objects.

Most modern languages support code analysis via annotations like @NonNull that checks references like arguments and will throw an exception, when a parameter is null/not initialized. javax.annotation for instance provides such annotations.

public void operation(@NonNull Object param) {
    param.toString(); // Guaranteed to be not null
}

Using such annotations can guard library code against null arguments.

Null-Object Pattern

Instead of having null references, you initialize each reference with a meaningful value or a dedicated null-object:

Define the Null-object contract (not required):

interface NullObject {
  public boolean getIsNull();
}

Define a base type:

abstract class Account {
  private double value;
  private List<Owner> owners;

  // Getters/setters    
}

Define the Null-object:

class NullAccount extends Account implements NullObject {

  // Initialize ALL attributes with meaningful and *neutral* values
  public NullAccount() {
    setValue(0); // 
    setOwners(new ArrayList<Owner>())

  @Override
  public boolean getIsNull() {
    return true;
  }
}

Define the default implementation:

class AccountImpl extends Account implements NullObject {

  @Override
  public boolean getIsNull() {
    return true;
  }    
}

Initialize all Account references using the NullAccount class:

class Employee  {
  private Account Account;

  public Employee() {
    setAccount(new NullAccount());
  }
}

Or use the NullAccount to return a failed state instance (or default) instead of returning null:

public Account findAccountOf(Owner owner) {
  if (notFound) {
    return new NullAccount();
  }
}

public void testNullAccount() {
  Account result = findAccountOf(null); // Returns a NullAccount
  
  // The Null-object is neutral. We can use it without null checking.
  // result.getOwners() always returns 
  // an empty collection (NullAccount) => no iteration => neutral behavior
  for (Owner owner : result.getOwners()) {
    double total += result.getvalue(); // No side effect.
  }
}
  

Try-Do Pattern

Another pattern you can use is the Try-Do pattern. Instead of testing the result of an operation you simply test the operation itself. The operation is responsible to return whether the operation was successful or not.

When searching a text for a string, it might be more convenient to return a boolean whether the result was found instead of returning an empty string or even worse null:

public boolean tryFindInText(String source, String searchKey, SearchResult result) {
  int matchIndex = source.indexOf(searchKey);
  result.setMatchIndex(matchIndex);
  return matchIndex > 0;
}

public void useTryDo() {
  SearchResult result = new Searchresult();
  if (tryFindInText("Example text", "ample", result) {
    int index = result.getMatchIndex();
  }
}

In your special case, you can replace the findByAAndB() with an containsAAndB() : boolean implementation.


Combining the patterns

The final solution implements the Null-Object pattern and refactors the find method. The result of the original findByAAndB() was discarded before, since you wanted to test the existence of A and B. A alternative method public boolean contains() will improve your code.
The refactored implementation looks as followed:

abstract class FieldA {

}

class NullFieldA {

}

class FieldAImpl {

}

class SomeClass {

  public SomeClass() {
    setFieldA(new NullFieldA());
  }
}

The improved validation:

public boolean isValid(SomeClass someObject, ConstraintValidationContext context) {
  return someRepository.containsAAndB(someObject.getFieldA(), B);
}
Related