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.
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
try method you could shorten this a little:
Ruby 2.3 introduced the safe navigation operator (AKA “lonely operator”) to take up even less space:
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
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 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:
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
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
When to use them
I do think there are occasional use cases. I tend to use
#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.