Naming Things is Hard
In the developers’ world, there is a well known quote by Phil Karlton that goes
There are only two hard things in Computer Science: cache invalidation and naming things. We usually think about that phrase in the sense that it’s hard to come up with a clear, descriptive, and concise name for the code we write (variables, methods/functions, modules/classes, etc), but sometimes, the perfect name we found can be a problem too.
With Great Power Comes Great Responsibility
Ruby allows us to do many things that could be difficult in other languages, we can modify classes and override methods on the fly, we can define new classes programmatically based on the result of other code, we can append/prepend/extend modules at runtime, we can do a lot of meta programming… and we can do that with no warnings or complains by the Ruby interpreter.
But we have to be careful, there’s a lot of power, but, if not used properly, we can create a lot of problems.
Reserved Words and Reserved Names
Here’s a list of some reserved words for the Ruby language:
alias and BEGIN begin break case class def defined? do else elsif END end ensure false for if module next nil not or redo rescue retry return self super then true undef unless until when while yield __FILE__ __LINE__
While we can define methods with many of those names, it would be hard to use them directly as methods and Ruby will complain in most cases because it expects specific syntax around those words. But not always! Maybe a method called
defined?(...) makes perfect sense in your application, but trying to use it will actually call Ruby’s
defined? method unless you use it like this:
self.defined?(...). A call to
defined?(...) will not fail (though it will probably return an unwanted result), and you’ll have no warning about it!
There are elements on the Ruby syntax that we can override. Did you know the
`cmd` syntax used to run system commands is actually a method of the Kernel module?
`ls` # => shows a list of files in the current directory using the "ls" system command
But we can define this method in one of our models for example:
class User def `(cmd) "I'm not running your command, sorry" end def run_ls `ls` end end User.new.run_ls # => "I'm not running your command, sorry"
This will probably break many things… We can have valid reasons for doing so (like adding custom behavior around executing system commands), but we have to be careful.
`cmd` example is extreme, but it’s just to show how easy it is to modify things that seem to be critical for Ruby. Any method we define in our objects can override methods defined in the Kernel module! Use the docs to check for method names that you should be careful when defining them https://ruby-doc.org/core-3.0.2/Kernel.html.
Conventions and Changing Conventions
Ruby on Rails follows the paradigm of Convention Over Configuration, that means that, if we name things in specific ways, and put things in specific places, the framework will do its “magic” and we’ll have all the good benefits.
This is really powerful, it enables us to write less code and get a lot of functionality for free. It’s one of the reasons why Rails makes building new apps so easy.
But again, there’s a catch here and we have to be careful, conventions are great but we can get unexpected behaviors if we don’t know them.
Another aspect to pay attention to about conventions is that they can change between Rails versions. With new features and configurations being added, a name that worked before may be a problem in the next Rails version.
TestController and test_helper
To illustrate this, we’ll share an issue we found during a Ruby on Rails upgrade.
We had to upgrade a Rails application from Rails 5.2 to 6.0. The application uses MiniTest so a
test_helper.rb file is used for the main point to configure the tests.
This application was also defining a controller named
TestController used only during tests. The name is clear, no doubts.
This worked fine with Rails 5.2 but generated a strange behavior in Rails 6.0: the
test_helper.rb was executed twice (leading to warnings about already initialized constants along with many other issues).
We found that Rails, following its convention for Rails 6.0, auto-includes helper files based on the name of the controller. So it was looking for a file named
test_helper.rb anywhere in the project to include it in the TestController class, not only in the
helpers folder. This didn’t happen in Rails 5.2!
The solution was simple: rename
TestController to something else like
The new name will work just fine… unless you define a file named
fake_helper.rbanywhere in your project, but know you know what to do.
Another place we need to be careful for names is on how we define method names and how we configure tools that define method names.
Sometimes, the domain of the problem we are solving uses specific words that we want to use for our classes or methods.
For example, if we are creating a game that allows cloning elements, we may be tempted to add a
clone method to one of our models.
The name makes sense, it describes the action using the language of the domain we are working with. But we have to be careful:
clone is a method already defined by ActiveRecord and also in the Kernel module. If we don’t know that, we’ll override a method, and Ruby won’t give us a hint that that is happening. Things may fail but the issue would be hard to find (some languages require specific syntax to override methods for example to prevent these accidental overrides, but we don’t have that un Ruby).
I won’t add a list of all the methods that are added by Rails by default in an ActiveRecord object, because the list is really long and it changes between Rails versions, but here’s a gist with the list of instance methods in Rails 7.0 as an example.
Code That Generates Methods
In a normal Rails app there can be a lot of metaprogramming going on in the background, adding methods based on configurations that are not always obvious.
For each value we add to our enum, ActiveRecord will define 2 instance methods and 2 scopes:
class Conversation < ActiveRecord::Base enum :status, [ :active, :archived ] end conversation.active! # setter conversation.active? # boolean check Conversation.active # positive scope Conversation.not_active # negative scope conversation.archived! conversation.archived? Conversation.archived Conversation.not_archived
But what happens if we add a
:frozen status? that will generate a
:frozen? method, but this is already defined by ActiveRecord internally in its core using a completely different logic!
A good practice is to use the suffix option on enums if you don’t want to think about this, you can find the documentation here.
A similar issue can be caused by the AASM gem: if we define a
frozen state, it will define a
frozen? method, clashing with the ActiveRecord’s one.
And this relates a bit to the previous section: Rails changed over time, and the
frozen? method overiden by AASM was used differently in Rails 4.1 and didn’t present any problem, but it created many issues in Rails 4.2 because of how Rails 4.2 makes use of that method in new places.
Again, there might be valid reasons to override those methods, but you should be careful and know what you are doing, and be prepared if Rails changes something internally!
Naming things is hard, and sometimes, the right name can be a problem. We don’t think about this all the time while coding (checking all these places every time we want to define a new method is a waste of time), but it’s important to know that these issues can happen and maybe it’s a good idea to try to remember some of the most generic ones.
Debugging a problem caused by unexpected overrides or changes in the gems we use can be really difficult, keep this in mind also when you have an issue and you see a suspicious method name in the stacktrace.