QR code

Object Validation: to Defer or Not?

  • Moscow, Russia
  • comments

OOPoop

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.

Punching the Clown (2009) by Gregori Viens
Punching the Clown (2009) by Gregori Viens

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.

sixnines availability badge   GitHub stars