Collecting Input
VALID_STATES = [
3.12 Reject unworkable values with preconditions
Indications
Some input values to a method cannot be converted or adapted to a usable form. Accepting them into the method has potentially harmful or difficult-to-debug side effects. Example: a hire_date of nil for an Employee object may result in undefined behavior for some Employee methods.
Synopsis
Reject unacceptable values early, using precondition clauses.
Rationale
It is better to fail early and obviously than to partially succeed and then raise a confusing exception.
Example: Employee hire dates
require 'date'
class Employee
attr_accessor :name
attr_accessor :hire_date
def initialize(name, hire_date)
@name = name
@hire_date = hire_date
end
def due_for_tie_pin?
raise "Missing hire date!" unless hire_date ((Date.today - hire_date) / 365).to_i >= 10
end
def covered_by_pension_plan?
# TODO Someone in HR should probably check this logic ((hire_date && hire_date.year) || 2000) < 2000
end def bio
if hire_date
"#{name} has been a Yoyodyne employee since #{hire_date.year}"
else
"#{name} is a proud Yoyodyne employee"
end end end
We can speculate about the history of this class. It looks like over the course of development, three different developers discovered that #hire_date might sometimes be nil. They each chose to handle this fact in a slightly different way. The one who wrote #due_for_tie_pin? added a check that raises an exception if
the hire date is missing. The developer responsible for
#covered_by_pension_plansubstituted a (seemingly arbitrary) default value for nil. And the writer of #bio went with an if statement switching on the presence of #hire_date.
This class has some serious problems with second-guessing itself. And the root of all this insecurity is the fact that the #hire_date attribute cannot be relied upon—even though it's clearly pretty important to the operation of the class! One of the purposes of a constructor is to establish an object's invariant: a set of properties which should always hold true for that object. In this case, it really seems like "employee hire date is a Date" should be one of those invariants. But the
constructor, whose job it is to stand guard against initial values which are not compatible with the class invariant, has fallen asleep on the job. As a result, every other method dealing with hire dates is burdened with the additional responsibility of checking whether the value is present.
This is an example of a class which needs to set some boundaries. Since there is no obvious "right" way to handle a missing hire date, it probably needs to simply insist on having a valid hire date, thereby forcing the cause of these spurious nil values to be discovered and sorted out.
We can maintain the integrity of this class by setting up a precondition checking the value of hire_date wherever it is set, whether in the constructor or elsewhere:
require 'date'
class Employee
attr_accessor :name
attr_reader :hire_date
def initialize(name, hire_date)
@name = name self.hire_date = hire_date
end
def hire_date=(new_hire_date)
raise TypeError, "Invalid hire date" unless
new_hire_date.is_a?(Date)
@hire_date = new_hire_date
end
def due_for_tie_pin?
((Date.today - hire_date) / 365).to_i >= 10
end
def covered_by_pension_plan? hire_date.year < 2000
end def bio
"#{name} has been a Yoyodyne employee since #{hire_date.year}"
end end
Here we are using a precondition to prevent an invalid instance variable from being set. But preconditions can be used to check for invalid inputs to individual methods as well.
def issue_service_award(employee_address, hire_date, award_date)
unless (FOUNDING_DATE..Date.today).include?(hire_date)
raise RangeError, "Fishy hire_date: #{hire_date}"
end
years_employed = ((Date.today - hire_date) / 365).to_i # $10 for every year employed
issue_gift_card(address: employee_address,
amount: 10 * years_employed)
end
Note that preconditions, as originally described by Bertrand Meyer in Object
Oriented Software Construction, are supposed to be the caller's responsibility: that is, the caller should never call a method with values which violate that method's preconditions. In a language such as Eiffel with baked in support for Design by Contract [DbC], the runtime ensures that this contract is observed, and raises an exception automatically when a caller tries to supply bad arguments to a method. In Ruby we don't have any built-in support for DbC, so we've moved the preconditions into the beginning of the protected method.
Executable documentation
Preconditions serve double duty. First and foremost, they guard the method from invalid inputs which might put the program into an undefined state. But secondly, because of their prominent location at the start of a method, they serve as
executable documentation of the kind of inputs the method expects. When reading the code, the first thing we see is the precondition clause, telling us what values are out of bounds for the method.
Conclusion
Some inputs are simply unacceptable. In some cases, this will just result in an error somewhere down the line, with no further harm done. But in other cases, bad inputs can cause real harm to the system and even to the business. More insidiously, code written in an atmosphere of fear and doubt about bad inputs can lead to multiple, inconsistent ways of handling unexpected values being developed. Decisively rejecting unusable values at the entrance to our classes can make our code more robust, simplify the internals, and provide valuable documentation to other developers.