Naming Things is Hard

Naming Things is Hard

In the developers’ world, there is a well known quote by Phil Karlton opens a new window 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!

Kernel Methods

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.

The `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 opens a new window .

Conventions and Changing Conventions

Ruby on Rails follows the paradigm of Convention Over Configuration opens a new window , 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 FakeController.

The new name will work just fine… unless you define a file named fake_helper.rb anywhere in your project, but know you know what to do.

Unknown Overrides

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.

Naming Methods

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 opens a new window 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.

Two common examples of this are ActiveRecord Enums opens a new window , and the AASM (Acts As State Machine) gem opens a new window .

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 opens a new window 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 opens a new window .

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!

Conclusion

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.

Get the book