I've come up with this problem over and over again when creating objects that must verify conditions. Should the checks be placed before trying to create the object or in the constructor of the object itself?
To illustrate it better, here's an example: let's say that we have a student manager, a professor, who adds students objects to a list of them. When creating a new student object, we must check that his name is maximum 20 characters long.
class Professor{
LinkedList<Student> studentsList;
Professor(){
studentsList = new LinkedList<Student>();
}
public Student addStudent(String studentName){
// Place 1
if (studentName.length <= 20)
studentList.add(new Student(studentName));
else
// Do another thing
}
}
class Student {
String name;
Student(String studentName){
// Place 2
if (studentName.length <= 20)
name = studentName);
else
// Don't create the object and throw exception
}
}
So basically my question would be, should the checks be done in "Place 1", before trying to create the student, or in "Place 2", in the constructor of students.
Objects take responsibility for themselves
Generally in object-oriented programming (OOP), we want objects to be responsible for themselves. Business rules regarding the integrity of their internal state should be handled, well, internally (or delegated to a builder -- see below). This idea is part of what is known formally in OOP as encapsulation.
So in your example, the Professor
class should not worrying about the Student
class’ rules such as length of the student’s name. The Student
class should be enforcing its own integrity. We want the logic for these integrity rules to be located in one single place, not spread out over your entire app.
Indeed, the Professor
class should not be instantiating the Student
object. Implied in your example is that there must be some other party that is assigning students to a professor. Perhaps a Tutorial
object that is responsible for tracking the assignment and progress of a few students being supervised by a professor. This Tutorial
should be instantiating the Student
objects, or passing on the Student
objects received from some other source such as a database-service object.
By the time the Student
objects reach the Professor
, they should be valid. The Professor
class should not be concerned with what makes a Student
valid or not. The Professor
should only be concerned with what makes a Professor
valid or not.
class Professor{
List< Student > students;
…
public void addStudent( Student student ){
Objects.requireNonNull( student , "Received NULL rather than a Student object. Message # 68a0ff63-8379-4e4c-850f-e4e06bd8378a." ) ; // Throw an exception if passed a null object.
Objects.requireNonNull( this.students , "Collection of Student objects is NULL. Message # c22d7b22-b450-4122-a4d6-61f92129569a." ) ; // Throw an exception if the `students` list is not established.
this.students.add( student ) ;
}
}
Besides the idea of objects being responsible for themselves, another reason for Professor
to not be instantiating Student
objects is to facilitate testing. If the Student
objects come from some other source, that source can provide faux objects using a Student
class or interface that is not yet finished, has certain functionality disabled (such as database access), or is substituted with bogus data designed for testing a scenario.
Builder pattern
If you have multiple properties that need verifying in order to instantiate a new object, you may want to use the Builder pattern. You define an additional class, such as StudentBuilder
that has methods for each of the parts needed to make a student.
Often, these methods all return the same StudentBuilder
object to facilitate call-chaining.
Different folks have different styles for a builder. One way is to provide a validity-checking method, and perhaps a method that provides a list of problems that prevent building the desired object.
Some people use a word like with
rather than the accessor method set
to make clear that while we are temporarily setting a property on the builder, the real intention is to be setting a property on an object of another class.
StudentBuilder sb = new StudentBuilder().withFirstName( "Alice" ).withLastName( "Coleman" ).withEmail( "[email protected]" );
if( sb.isValid() ) {
Student s = sb.build() ;
…
}