Thinking About Types 2014-07-24
Take a look at the following piece of code:
class Operations::Operation
ALLOWED_OPERATORS = [
# ...
]
attr_reader :operator
def initialize(operation)
@operator = operation[:operator]
# ...
validate_operator
end
def execute
send(operator)
# ...
end
private
def validate_operator
raise ArgumentError, "No such operator: #{operator}" unless ALLOWED_OPERATORS.include?(operator)
end
end
Where do you think that validate_operator
should go? Inside the initialize
or execute
method?
This was a real discussion that I saw a while ago and the rest of this post will
focus on why the best place to call validate_operator
is inside initialize
.
The class is supposed to represent an operation based on the operator that’s
passed to it in the constructor. A well constructed system shouldn’t allow the
construction of invalid operations and this is what validate_operator
does.
In a statically typed language we would define the set of possible operations based on the allowed operators and if somewhere in our code we tried to create an invalid operation the type checker would most likely tell us that the program has an error and it would show where this error came from before executing the program itself.
But as we know Ruby is a dynamically typed language1, we would only know that
an operation is invalid at runtime and we might have two different scenarios of
when we would get this error depending on where we call validate_operator
.
Let’s look at both scenarios:
-
Inside
execute
: we would create an invalid operation just fine and only hit an error when we calledexecute
. -
Inside
initialize
: whenever we created a new invalid operation we would hit an error instantly.
Let’s analyze both scenarios in order to see which one would be easier to fix any possible problems caused by the creation of invalid operations. Consider that operations can and will be passed along other classes and methods.
In scenario #1 we can think about invalid operations as being a time bomb in our
hands, floating around, just waiting the call to execute
before exploding an
error in our face. In such scenario we would need to trace all the way back,
passing between classes and methods, to the exact point where such operation
was created.
In scenario #2 the error would be reported right at the point of creation of such invalid operation. This would save us time because we wouldn’t need to search the point of creation of the invalid operation like we would have to do in scenario #1.
The point is: thinking about what are the valid types in your system and enforcing that it can only create such types can reduce the number of possible bugs and also save time when dealing with errors. So, next time you write something think about it as if you were a type checker, would you allow invalid types to be created?
Notes
-
Well, kinda: http://existentialtype.wordpress.com/2011/03/19/dynamic-languages-are-static-languages/ ↩