Last week, in an effort to level-up my knowledge of Ruby style conventions, I connected Code Climate and installed the RuboCop gem in a few of the older projects I’ve been refactoring. As I expected with a new install in existing apps, RuboCop presented me with a backlog of roughly 300 items to address in each app.

The most interesting suggestions were in a SessionsController because I got to learn about the Safe Navigation Operator, or &., which led to learning about using the Null Object Pattern.

In the SessionsController below, you’ll see the first RuboCop suggestion (i.e. the tip of the iceberg):

# app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  ...
  def create
    user = User.find_by(email: params[:email])

    # RUBOCOP says: Style/SafeNavigation: Use SafeNavigation
    if user && user.authenticate(params[:password])
      # [log the user in]
    else
      # [re-render the sign-in form and show an error message]
    end
  end
  ...
end

So what is this Style/SafeNavigation suggestion about? The Safe Navigation Operator returns nil if a method is called on a nil object instead of throwing an error.

In his post, The Safe Navigation Operator (&.) in Ruby, Georgi Mitrev has a nice explanation of the case for it:

Imagine you have an account that has an owner and you want to get the owner’s address. If you want to be safe and not risk a Nil error you would write something like the following:

if account && account.owner && account.owner.address
  ...
end

We can rewrite the previous example using the safe navigation operator:

if account&.owner&.address
  ...
end

Now that’s some pretty nifty shorthand for an otherwise long string of &&s. So in my case, this is the style change RuboCop is suggesting:

if user&.user.authenticate(params[:password])
  ...
end

Right? Not quite. When I implemented it, RuboCop said:

Lint/SafeNavigationChain: Do not chain ordinary method call after safe navigation operator.

Okay… thanks for the sweet tip on &., but what do you want from me, RuboCop???

Better object-oriented programming, my friends. Better OOP. Are you down with OOP? lol.

The Null Object Pattern Solution

In a pairing session, I was able to see the bigger situation here, which is that my code is asking an object about whether it exists when it could just be handing the nil value in a predicted way. What if user.authenticate(params[:password]) could just safely run regardless of whether an instance of the User class exists?

That would be nice. And to make that dream a reality, we need a null user object. As Sandi Metz explains in her Nothing is Something talk, this object will stand in as a legitimate object where we’d otherwise have a nil. Since we will have built this custom object, we get to tell it how to react to situations that would otherwise result in a nil value and subsequent error.

Running this in the console, we get:

>> user = User.find_by(email: "email-that-doesn't-exist")

>> user
=> nil

>> user.authenticate(params[:password])
!! #<NoMethodError: undefined method `authenticate' for nil:NilClass>

The Guaranteed User

The controller doesn’t want to manage nil users. It’s busy and has its own stuff to do. It wants a guaranteed user to work with. The controller doesn’t care by what means this guaranteed user happens, it just needs one in order to move forward.

Instead of creating a user variable with the potential of equaling nil because a matching record isn’t found in the database, our user variable will equal some safe object provided to us by our new GuaranteedUser class. Then, since we don’t have to safeguard against nils, we can safely run the user.authenticate method.

# app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  ...
  def create
    # Instead of a potentially nil instance of the User
    # class, we're guaranteeing an actual object which is
    # provided by the GuaranteedUser class
    user = GuaranteedUser.find_by(email: params[:email])

    # Now that we're not worried about managing nils, we can run
    # the authenticate method without other safeguards in place
    if user.authenticate(params[:password])
      # [log the user in]
    else
      # [re-render the sign-in form and show an error message]
    end
  end
  ...
end

If the user doesn’t actually exist in the users table, how can we guarantee a user object?

We’re not guaranteed to get a User object, but we’re guaranteed to get a user-like object, and that’s really all we need. The GuaranteedUser class searches the users table and if it finds a matching user, it returns that User object. If there is no user that matches, it returns a NullUser object. Either outcome supplies us with the user-like object that we need.

# app/models/guaranteed_user.rb

class GuaranteedUser
  def self.find_by(params)
    User.find_by(params) || NullUser.new
  end
end


# app/models/null_user.rb

class NullUser
end

In the console, we try to find a user via its email address. When we cannot find a match, we get an object instead of a nil value:

>> user = GuaranteedUser.find_by(email: "email-that-doesn't-exist")

>> user
=> #<NullUser:0x00007f88130dbc70>

But how is user.authenticate safe to run now?

It’s not yet, but after we create an authenticate method for NullUser, it will be. We’ll make the method return false just like the authenticate method from the User class would if a user that actually existed didn’t pass authentication. This method will always return false because a NullUser will never pass authentication.

# app/models/null_user.rb

class NullUser
  def authenticate(_)
    false
  end
end

Running this in the console, we get:

>> user = GuaranteedUser.find_by(email: "email-that-doesn't-exist")

>> user
=> #<NullUser:0x0007fed23436d6>

>> user.authenticate(params[:password])
=> false

The generated NullUser object fails authentication, the sign-in form is re-rendered in the browser, and the human user sees an error about incorrect credentials.

In the case of a legitimate user (an email that does exist in the users table) that fails authentication because of the wrong password, in the console we get:

>> user2 = GuaranteedUser.find_by(email: 'sample@gmail.com')
=> #<User id: 2, name: "Actual User", email: "sample@gmail.com", created_at: "2016-01-26 18:05:14", updated_at: "2018-08-20 10:03:44">

>> user2.authenticate('wrong-password')
=> false

The User object fails authentication, the sign-in form is re-rendered in the browser, and the human user sees an error about incorrect credentials. The key point here is regardless of whether the user variable is pointed to an instance of User or NullUser, the rest of the process flows in the same way.

This approach makes for a much cleaner controller. It also leaves decisions about what defines a GuaranteedUser and a NullUser up to their own classes to handle. Since we’ve wrapped the concept of a user-like object in a reliable API called GuaranteedUser, we can access it confidently throughout our codebase and limit the number of conditions we’d otherwise have to write.

Taking this beyond the SessionsController

I personally haven’t implemented anything beyond what my needs were for this controller yet. However, have done so is making me think about other possible uses for this guaranteed user object. I can think of a couple of projects I’ve worked on where I needed a “guest” user to keep my views from blowing up when no logged-in user was present. Using this pattern, gets me a lot closer to what I wanted to have happen in my views:

# app/models/null_user.rb

class NullUser
  ...
  def name
    'Guest'
  end
end
<!-- app/views/some_view.html.erb -->

<!-- I'd get to do this: -->
<h1>Hello, #{@user.name}!</h1>

<!-- Instead of this: -->
<% if @user %>
  <h1>Hello, #{@user.name}!</h1>
<% else %>
  <h1>Hello, Guest!</h1>
<% end %>

Though I still have to work out the rest of the details of making this happen, this Null Object Pattern concept feels like a solid foundation on which to work.


Side Note:

A big chunk of those 300ish RuboCop suggestions belonged to the Style/FrozenStringLiteralComment category, which is to prepare my Ruby 2.5.0 codebase for upcoming changes in Ruby 3.0. If you find yourself staring down the prospect of adding the magic comment line:

# frozen_string_literal: true

to the top of almost all of the files in your Rails app like I did, don’t worry, there’s a gem for that.