carlosgaldino · home

Thinking About Types

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:

  1. Inside execute: we would create an invalid operation just fine and only hit an error when we called execute.

  2. 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

Carlos Galdino
@carlosgaldino
github.com/carlosgaldino