Object Validation: to Defer or Not?
I said earlier that constructors must be code-free and do nothing aside from attribute initialization. Since then, the most frequently asked question is: What about validation of arguments? If they are “broken,” what is the point of creating an object in an “invalid” state? Such an object will fail later, at an unexpected moment. Isn’t it better to throw an exception at the very moment of instantiation? To fail fast, so to speak? Here is what I think.
Let’s start with this Ruby code:
class Users { def initialize(file) @file = file end def names File.readlines(@file).reject(&:empty?) end }
We can use it to read a list of users from a file:
Users.new('all-users.txt').names
There are a number of ways to abuse this class:
- Pass
nil
to the ctor instead of a file name; - Pass something else, which is not
String
; - Pass a file that doesn’t exist;
- Pass a directory instead of a file.
Do you see the difference between these four mistakes we can make? Let’s see how our class can protect itself from each of them:
class Users { def initialize(file) raise "File name can't be nil" if file.nil? raise 'Name must be a String' unless file.is_a?(String) @file = file end def names raise "#{@file} is absent" unless File.exist?(@file) raise "#{@file} is not a file" unless File.file?(@file) File.readlines(@file).reject(&:empty?) end }
The first two potential mistakes were filtered out in the constructor, while the other two—later, in the method. Why did I do it this way? Why not put all of them into the constructor?
Because the first two compromise object state, while with the other two—its runtime behavior. You remember that an object is a representative of a set of other objects it encapsulates, called attributes. The object of class Users
can’t represent nil
or a number. It can only represent a file with a name of type String
. On the other hand, what that file contains and whether it really is a file—doesn’t make the state invalid. It only causes trouble for the behavior.
Even though the difference may look subtle, it’s obvious. There are two phases of interaction with the encapsulated object: connecting and talking.
First, we encapsulate the file
and want to be sure that it really is a file. We are not yet talking to it, we don’t want it to work for us yet, we just want to make sure it really is an object that we will be able to talk to in the near future. If it’s nil
or a float
, we will have problems in the future, for sure. That’s why we raise an exception from the constructor.
Then the second phase is talking, where we delegate control to the object and expect it to behave correctly. At this phase we may have other validation procedures, in order to make sure our interaction will go smoothly. It’s important to mention that these validations are very situational. We may call names()
multiple times and every time have a different situation with the file on disc. To begin with it may not exist, while in a few seconds it will be ready and available for reading.
Ideally, a programming language should provide instruments for the first type of validations, for example with strict typing. In Java, for example, we would not need to check the type of file
, the compiler would catch that error earlier. In Kotlin we would be able to get rid of the NULL check, thanks to their Null Safety feature. Ruby is less powerful than those languages, that’s why we have to validate “manually.”
Thus, to summarize, validating in constructors is not a bad idea, provided the validations are not touching the objects but only confirm that they are good enough to work with later.
Published on Java Code Geeks with permission by Yegor Bugayenko, partner at our JCG program. See the original article here: Object Validation: to Defer or Not? Opinions expressed by Java Code Geeks contributors are their own. |