Replacing exceptions with Notification in validations
A common way to approach validation is to run series of checks on some data. If any of these checks fails, you raise an exception with an error message.
I have a couple of problems with this approach. Firstly I'm not happy with using exceptions for something like this. Exceptions signal something outside the expected bounds of behavior of the code in question. But if you're running some checks on outside input, this is because you expect some messages to fail - and if a failure is expected behavior, then you shouldn't be using exceptions.
The second problem with code like this is that it fails with the first error it detects, but usually it's better to report all errors with the incoming data, not just the first. That way a client can choose to display all errors for the user to fix in a single interaction.
My preferred way to deal with reporting validation issues like this is the Notification pattern. A notification is an object that collects errors, each validation failure adds an error to the notification. A validation method returns a notification, which you can then interrogate to get more information.
When to use this refactoring
Exceptions are a very useful technique for handling exceptional behaviour and getting it away from the main flow of logic. This refactoring is a good one to use only when the outcome signaled by the exception isn't really exceptional, and thus should be handled through the main logic of the program. The example I'm looking at here, validation, is a common case of that.
Whether to use exceptions for a particular task is dependent on the context. So, as the prags go on to say, reading from a file that isn't there may or may not be an exception depending on the circumstances. If you are trying to read a well known file location, such as /etc/hosts on a unix system, then it's likely you can assume the file should be there, so throwing an exception is reasonable. On the other hand if you are trying to read a file from a path that the user has typed in on the command-line, then you should expect that it's likely the file isn't there, and should use another mechanism - one that communicates the unexceptional nature of the error.
There is a case when it may be sensible to use exceptions for validation failures. This would be situations where you have data that you expect to have already been validated earlier in processing, but you want to run the validation checks again to guard against a programming error letting some invalid data slip through.
Example
require 'date'
class BookingRequest
def initialize(number_of_seats:, date:)
@number_of_seats = number_of_seats
@date = date
end
def check
raise ArgumentError, validation.error_message if validation.has_errors?
end
def validation
note = Notification.new
validate_date note
validate_number_of_seats note
note
end
private
def validate_date(note)
return note.add_error 'date is missing' if date.nil?
begin
parsed_date = Date.parse date
rescue Exception => exp
note.add_error 'Invalid format for date', exp
return
end
note.add_error 'date cannot be before today' if parsed_date < Date.today
end
def validate_number_of_seats(note)
return note.add_error 'number of seats cannot be null' if number_of_seats.nil?
note.add_error 'number of seats must be positive' if number_of_seats < 1
end
attr_reader :number_of_seats, :date
end
require 'set'
class Notification
def initialize
@errors = Set.new
end
def add_error(message)
add_error message, nil
end
def add_error(message, exception)
@errors.add Error.new(message, exception)
end
def has_errors?
!@errors.empty?
end
def error_message
@errors.map(&:message).join(', ')
end
private
class Error
def initialize(message:, cause:)
@message = message
@cause = cause
end
end
end