Martijn

Martijn Versluis – 5 December 2018
629 words in about 3 minutes

Ruby and Rails include some techniques to “safely navigate” objects. However, using those techniques can lead to bad code.

safe navigation

First, let’s find out what safe navigation in Ruby looks like. Consider the following code that tries to get the avatar for the first group a user is member of:

1
2
3
def first_group_avatar_url
  user.groups.first.avatar.file if user.groups.first && user.groups.first.avatar
end

Using ActiveSupport’s try method you could shorten this a little:

1
user.groups.try(:first).try(:avatar).try(:file)

Ruby 2.3 introduced the safe navigation operator (AKA “lonely operator”) to take up even less space:

1
users.groups&.first&.avatar&.file

I dislike all approaches, and here are five reasons why:

Reason #1: it is hard to read

No matter which variant you choose, the code looks cryptic and maybe even awkward. Besides that, when coming from a different programming language, it might even be a totally new concept to you.

Reason #2: it obscures intent

The code tells you something in the chain can be nil, but it does not tell you why. That makes it hard for another developer to change something in the code. You can add simple methods that explain it. See how this clearly explains the possible nils?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class User
  def member_of_a_group?
    groups.any?
  end
  
  def first_group_avatar
    user.groups.first.avatar.file if member_of_a_group? && groups.first.has_avatar?
  end
end

class Group
  def has_avatar?
    !avatar.nil?
  end
end

Reason #3: it attracts bad code

Just like all things that can be nil, the nil leaks into all code that uses it. Imagine some helper that wants to determine the URL for the group avatar. The avatar can be nil, so a new method call should deal with a possible nil. It is like a broken window attracting more broken windows.

1
2
3
4
avatar_url = user.first_group_avatar.try(:url)

# or:
avatar_url = user.first_group_avatar&.url

Reason #4: it (tends to) violate Demeter

Because using safe navigation operators make it easy to chain lots of method calls, it is also easy for violations to end up in the code. For example, you would determine the avatar URL for a group admin:

1
group.admin&.avatar&.url

However, according to the Law of Demeter, we should not have knowledge about group.admin, so in fact responsibility should be spread across method and objects and we should just call group.admin_avatar_url.

1
2
3
4
5
6
7
8
9
10
11
class Group
  def admin_avatar_url
    admin.avatar_url if admin
  end
end

class Admin
  def avatar_url
    avatar.url if avatar
  end
end

It you want, you can make the code more concise using ActiveSupport delegators. Personally, I find the vanilla version above easier to read though.

1
2
3
4
5
6
7
class Group
  delegate :avatar_url, to: :admin, prefix: true, allow_nil: true
end

class Admin
  delegate :url, to: :avatar, prefix: true, allow_nil: true
end

Reason #5: it makes bug hunting harder

Let’s say we implemented first_group_avatar_url using the “lonely operator”:

1
2
3
def first_group_avatar_url
  users.groups&.first&.avatar&.url
end

Some part of the application uses our method, but you get a “undefined method for nil:NilClass” error. How will you find out which object was nil? Maybe the user was not a member of any group, maybe the group did not have an avatar, or maybe the avatar url was empty. Using safe navigation makes it harder to track down the cause of a NoMethodError.

When to use them

I do think there are occasional use cases. I tend to use &. or #dig when exploring a data structure inside IRB. The same applies to creating a prototype or quickly trying out an API integration.

Still navigating safe?

I hope I convinced you how to deal with nils in a proper way. Do you want to share your thoughts on this, or do you have questions? Please let me know.

At Kabisa we know a lot about writing great code. Leave us a message if you would like to get in touch.