Command-Query Separation in Ruby
One of the features of Ruby that can cause some confusion is that just about everything is an expression. This means that nearly everything that can return a value, does. This leads to some very interesting and sometimes confusing code.
weather = if season == "Winter"
"cold"
else
"warm"
end
The previous example is contrived because you would typically write it using the ternary operator, but you get my point. Things get even more interesting when it comes to method definitions. Take a look at this method, what does it return?
def lock_door
@door_state = :locked if @door_state == :closed
end
It would return :locked if the door was closed, otherwise it would return nil. Is
this useful? Not really. What should we do here? Always return nil regardless of if the
action was taken or not? Return the final state of the door? I'd propose neither. Leave the method how
it is. Bertrand Meyer's principle of Command-Query Separation
states that every method should either be a command that performs an action or a query that returns data
to the caller. Martin Fowler has a slightly different take
on the matter. He prefers to call commands "modifiers" because they are changing the state of the object
they are being called on.
To apply this principle to Ruby I like to think in term of grammar. If a method is a verb you shouldn't care what it returns. If a method is a noun or adjective, it had better not change any state.
Like most rules, they are meant to be broken. Martin mentions popping from a stack as a good example. You don't need to look any further than Rails to find some other nice caveats:
p customer.errors unless customer.valid?
The valid? is a query, yet it mutates the object by populating the errors array. I think this is a very
pragmatic decision. If we were to refactor this code to follow Command-Query Separation to the letter, it would look
something like this:
customer.validate
p customer.errors unless customer.valid?
The problem is, you would almost never have a reason to call validate and never call valid?
to see if it worked correctly. Fine choice Rails. Here is another caveat:
customer = Customer.create! :name => "Tony"
This is also a pragmatic approach because if create! did not return the object, we would have to make
another round trip to the database to retrieve it.
Command-Query Separation is an important thing to keep in mind. This is doubly true when writing Ruby code because every method returns something whether you like it or not. So if you are going to break the rule, have a good reason.
Update: fixed some typos