Let’s assume we have a class Student with the following constructor:
/** Initializes a student instance.
* @param matrNr matriculation number (allowed range: 10000 to 99999)
* @param firstName first name (at least 3 characters, no whitespace)
*/
public Student(int matrNr, String firstName) {
if (matrNr < 10000 || matrNr > 99999 || !firstName.matches("[^\\s]{3,}"))
throw new IllegalArgumentException("Pre-conditions not fulfilled");
// we're safe at this point.
}
Correct me if I’m wrong, but I think in this example, I followed the design by contract paradigm by simply specifiying the (rather static) constraints on the possible input values and raising a generic, unchecked exception if those are not fulfilled.
Now, there is a backend class that manages a list of students, indexed by their matriculation number. It holds a Map<Integer, Student> to save this mapping and provides access to it through an addStudent method:
public void addStudent(Student student) {
students.put(student.getMatrNr(), student);
}
Now let’s assume there is a constraint on this method like “a student with the same matriculation number must not already exist in the database“.
I see two options of how this could be realized:
Option A
Define a custom UniquenessException class that is raise by addStudent if a student with the same matr. number already exists. Calling code will then look something like this:
try {
campus.addStudent(new Student(...));
catch (UniquenessError) {
printError("student already existing.");
}
Option B
State the requirement as a pre-condition and simply raise an IAE if it doesn’t hold. Additionally, provide a method canAddStudent(Student stud) that checks in advance whether addStudent will fail. Calling code would then look something like this:
Student stud = new Student(...);
if (campus.canAddStudent(stud))
campus.addStudent(stud);
else
printError("student already existing.");
I feel that option A is much cleaner from a software-engineering point of view, for at least the following reason:
- It can easily be made thread-safe without modifying the calling code (Thanks to Voo for pointing me to TOCTTOU, which seems to describe that exact issue)
Thus I wonder:
- Is there a third option which is even better?
- Does option B have an advantage that I didn’t think of?
- Would it actually be allowed from a design by contract point of view to use option B and define the uniqueness as a pre-condition of the
addStudentmethod? - Is there a rule of thumb when to define pre-conditions and simply raise
IAEand when to use “proper” exceptions? I think “make it a pre-condition unless it depends on the current state of the system” could be such a rule. Is there a better?
UPDATE: It seems like there is another good option, which is to provide a public boolean tryAddStudent(...) method that doesn’t throw an exception but instead signals error/failure using the return value.
(this is too long for a comment)
In your option B, I wouldn’t use a Map<Integer,Student> and then do:
The Map abstraction isn’t practical enough for your use case (you’re mentionning concurrency issues), I’d use instead a ConcurrentMap<Integer,Student> and do something like this: