Member-only story
Freeze Your Constants in Ruby
Years ago, I worked on a very large Ruby on Rails codebase that used constants to hold lists of credit card transaction states. For example:
class Txn
ACTIONABLE_STATES = [:authenticated, :to_settle]
DONE_STATES = [:settled, :declined]
end
However, we had a bug where a settled transaction would return true
when txn.state.in? ACTIONABLE_STATES
was called on it.
txn.state
# :settledtxn.state.in? ACTIONABLE_STATES
# true
This was obviously wrong because :settled
is not in ACTIONABLE_STATES
! After hours of searching, we found the offending code in a completely different part of the codebase.
def some_method
all_states = Txn::ACTIONABLE_STATES.concat(Txn::DONE_STATES)
all_states.each do |state|
# ...
end
end
If you’re already knowledgeable of the Ruby Array
class, you’ve probably spotted the offending line already. #concat
mutates the original list, even though it doesn’t end with the idiomatic !
, and because ACTIONABLE_STATES
is held statically in memory, when this code gets executed it changes ACTIONABLE_STATES
state for the rest of that Ruby virtual machine’s life.
In other words, after some_method
gets executed, ACTIONABLE_STATES
becomes[:authenticated, :to_settle, :settled, :declined]
until you…