Skip to content

Make method pre-conditions explicit by design

A method usually makes assumptions about the arguments that are passed into it by a method invocation. The conditions that the arguments must fullfil are named the method’s pre-conditions. Thus a good method implementation checks if the arguments are in an expected state so that the method can execute.

Often developers use basic data types as method parameters like String, Integer and so on, but a method usually doesn’t allow every possible value that these types can take. E.g. a method might declare an integer as parameter, but only accepts positive integers. Such a method might validate it’s pre-condition in this way:

public double divide(double dividend, double divisor){
 
    if(divisor == 0){
        throw new IllegalArgumentException("division by 0 is not allowed");
    }

    ...
}

Things will become more tricky the more parameters a method declares and even a lot more tricky if one parameter’s validation affects another. E.g.

public double doSomething(int arg1, ing arg2, double arg3, double arg4){

    if(arg1 < 0){
       if(arg2 <0 || arg3 < 0){
           throw new IllegalArgumentException("arg2 and arg3 must be 0 or greater when arg1 is less then 0");
       }
    } else if(arg1 >= 0 && arg4 < 0){
       ....
    } else if(...){
       ...
    }
    ....

}

More restrictive “primitive” types

Often a first attempt can be to introduce types that are more restrictive. E.g.

public class PositiveInteger extends Number {

  private int value;

  public PositiveInteger(int value) {
    if (value < 0) {
      throw new IllegalArgumentException("value must be a positive integer");
    }
    this.value = value;
  }

  @Override
  public int intValue() {
    return value;
  }

  @Override
  public long longValue() {
    return value;
  }

  @Override
  public float floatValue() {
    return value;
  }

  @Override
  public double doubleValue() {
    return value;
  }
}

A method that declares such a parameter can be changed from

public void doSomething(Integer value){
   if(value == null){
      throw new IllegalArgumentException("value must not be null");
   }

   if(value < 0){
      throw new IllegalArgumentException("value must be a positive integer");
   }

   ...
}

to

public void doSomething(PositiveInteger positiveValue){
   if(positiveValue == null){
      throw new IllegalArgumentException("positiveValue must not be null");
   }
   int value = positiveValue.intValue(); // We can be sure that the value is positive 

   ...
}

The more restrictive type approach is often usefull when using objects like String, because strings can take a lot of different values and it might be helpful to reduce the possible values that it can take. E.g. by introducing an `EMail` type that checks via a regular expression.

Pre-condition Duplication

The most simple pre-condition checks are also the ones that are usually duplicated. E.g. not null or positive integer checks.

public void append(String str){
   if(str == null){
      throw new IllegalArgumentException("str must not be null");
   }
   ...
}

public void append(char[] chars){
   if(chars == null){
      throw new IllegalArgumentException("chars must not be null");
   }
   ...
}

To avoid pre-condition duplication you can use

Avoid Duplication using a utility class

Pre-condition duplications can often be prevented using a utility class with static methods. E.g.

public class ArgumentAssertion {

    public static void notNull(Object obj, String name){
        if(obj == null){
            String msg = MessageFormat.format("{0} must not be null", name);
            throw new IllegalArgumentException(msg);
        }
    }

}

So the code above would change to

public void append(String str){
   ArgumentAsserttion.notNull(str, "str");
   ...
}

public void append(char[] chars){
   ArgumentAsserttion.notNull(chars, "chars");
   ...
}

As of Java 1.7 you can also use java.util.Objects. E.g

public void append(String str){
   Objects.requireNonNull(str, "str must not be null");
   ...
}

public void append(char[] chars){
   Objects.requireNonNull(chars, "chars must not be null");
   ...
}

But a utility class also has disadvantages

  • Global Dependency
    Since the utility class is used all across the objects, the objects all depend on it. It is a global dependency and changing the utility class might affect every class.
  • Stack trace confusion
    Since the exception is thrown in the utility class the utility class is the first stack trace element and not the class that really took the arguments. The stack trace might confuse you more if the utility class itself delegates to other methods of itself.If the method itself throws the exception the stackstrace will look like this:

    Exception in thread "main" java.lang.IllegalArgumentException: str must not be null
    	at MyStringBuilder.append(MyStringBuilder.java:12)
    	at MyStringBuilder.main(MyStringBuilder.java:7)

    compared to using a utility class

    Exception in thread "main" java.lang.IllegalArgumentException: str must not be null
    

    at ArgumentAssert.notNull(ArgumentAssert.java:8)

    	at MyStringBuilder.append(MyStringBuilder.java:11)
    	at MyStringBuilder.main(MyStringBuilder.java:7)

Avoid duplication using a parameter object

A parameter object is a design pattern the describes the usage of an object to represent parameters passed to a method.

The main concept of object-oriented programming is to bring logic and data together and the object can ensure that the data is in a leagal state at any time, because it controls the access to it.

In the case of method parameter validation it means that the validation logic belongs to the parameters. Thus the logic can be moved to a parameter object.

Dependent parameters validation

If 2 parameters belong together we can group them in an own object to make this relationship clear. Furthermore we can also validate the 2 parameter values if they depend on each other. E.g.

public class DateRange {

	private Date start;
	private Date end;

	public DateRange(Date start, Date end) {
		if (start == null) {
			throw new IllegalArgumentException("start must not be null");
		}
		if (end == null) {
			throw new IllegalArgumentException("end must not be null");
		}
		if (end.before(start)) {
			throw new IllegalArgumentException("end must be start or after start");
		}
		this.start = start.clone(); // clone is necessary because a Date object is mutable
		this.end = end.clone();
	}

	public Date getStart() {
		return start.clone(); // clone is necessary because a Date object is mutable
	}

	public Date getEnd() {
		return end.clone();
	}
}

Optional parameters as properties

When using a parameter object we can also make clear by design which parameters are mandatory and which are optional.

public class PersonParameters {

     private String firstName;
     private String lastName;

     private Date birthday;

     // Mandatory parameters will be constructor params of the parameter object
     // so that a parameter object can only be instantiated if this parameters
     // exist.
     public PersonParameters(String firstName, String lastName){ 
         if(firstName == null){
             throw new IllegalArgumentException("firstName must not be null");
         }
         if(firstName.length < 2){
              throw new IllegalArgumentException("firstName must contain at least 2 characters");
         }

         if(lastName == null){
             throw new IllegalArgumentException("lastNamemust not be null");
         }
         if(lastName.length < 2){
              throw new IllegalArgumentException("lastName must contain at least 2 characters");
         }

         this.firstName = firstName;
         this.lastName = lastName;
     }

     // Optional parameters as properties that can be set
     public void setBirthday(Date birthday){
         // more validation logic can be placed here
         this.birthday = birthday.clone();
     }

}

Conclusion

My conclusion is that that using parameter objects is a good practice, because you can express a method’s pre-conditions in an object. A method can be sure that if the parameter object exists (not null) it is in a legal state so that processing can go on. Furthermore the parameter object moves most of the validation from the method that uses it to the client that invokes the method, because the client must create the parameter object.

Disadvantages only occur if you implement a method’s specific validation logic in a parameter object that you use accross different methods. In this case you make these methods depend on each other. As long as the validation logic only applies to the parameter object’s data itself (like the DataRange or PositiveInteger type above) you can safely use them accross multiple methods.

I have written an own blog that deals with this subject. The blog is about service layer designs, but parts of it applies to other design problems too. Take a look at Pros and cons of service layer designs, especially the section Dedicated request and response types per service method.

Leave a Reply

Your email address will not be published. Required fields are marked *

 

GDPR Cookie Consent with Real Cookie Banner