nil values are the worst. They crop up where you didn’t expect them. Their presence forces you to litter your code with conditionals: checking, checking, and checking again if a variable contains nil .
Today I’m making available a classic screencast from the RubyTapas archives. In it you’ll see a way to remove nils while dramatically simplifying some typical web application logic, using the Special Case Pattern.
But! There’s more. Today by special permission I’m also bringing you a video from the Upcase Weekly Iteration archives! In this bonus video, you’ll watch Ben Orenstein and Joe Ferris apply a variant of the Special Case pattern, the Null Object pattern, in order to clean up some Rails 4 code.
Enjoy!
So that’s the Special Case pattern.
Sometimes, you have a special case where the “special” behavior you need is defined as: “always do nothing”. There’s a name for this “special case of Special Case”: the Null Object Pattern.
Now here’s Ben Orenstein and Joe Ferris of Upcase, with a demonstration of how to apply the Null Object pattern to a Rails project:
Did you enjoy this pairing of short & sweet RubyTapas with longer, more Rails-centric Upcase content?
If so, I have some very exciting news for you:
This week only, you can get an entire year of RubyTapas AND Upcase for a fantastic price. Go here to find out more.
There are also some nifty bonuses to be had, as well as special deals for teams.
I’ve never done a bundle like this before (and I don’t know if/when I’ll do it again). The deal expires February 6, 2017. If you want to supercharge your Ruby, Rails, object design, refactoring, and testing skills this year… go check it out!
Episode Script
For those who prefer reading to watching. This is for the RubyTapas episode only, not the Upcase video.
Here’s a typical helper method from a web application. It looks to find a current user by checking in a session
object. If it can find one it returns it; otherwise it implicitly returns nil
.
def current_user
if session[:user_id]
User.find(session[:user_id])
end
end
Let’s look at some places where this method is used.
Here’s some code to greet the user when they first arrive at the site. It checks to see if there is a current user. If so it uses their name; otherwise it uses the name “guest”.
def greeting
"Hello, " +
current_user ? current_user.name : "guest" +
", how are you today?"
end
Here’s a case where we render either a “log in” or “log out” button depending on whether there is a user.
if current_user
render_logout_button
else
render_login_button
end
Here’s some code that optionally renders an admin panel. Before it can check to see if the user is an admin, it first has to check that there is a user at all.
if current_user && current_user.has_role?(:admin)
render_admin_panel
end
In some cases we might want to show different results depending on what a user is allowed to see. Once again, we switch on the presence of a user object.
if current_user
@listings = current_user.visible_listings
else
@listings = Listing.publicly_visible
end
# ...
So far we’ve just been querying the user object. Here’s some code that updates an attribute on the user. But first it has to check that the user is non-nil.
if current_user
current_user.last_seen_online = Time.now
end
Here’s a snippet of code that adds a product to the user’s shopping cart. If they are logged in, it should go into their persistent user cart. Otherwise it should go into a special session-based cart.
cart = if current_user
current_user.cart
else
SessionCart.new(session)
end
cart.add_item(some_item, 1)
All of the code we’ve been looking at has a common characteristic: it’s uncertain about whether there will be a user object available. As a result, it keeps checking over and over again.
Let’s see if we can get rid of this uncertainty. Instead of representing an anonymous session as a nil
value, let’s write a class to represent that case. We’ll call it GuestUser
.
class GuestUser
def initialize(session)
@session = session
end
end
We rewrite the #current_user
method to return an instance of this class when there is no :user_id
recorded.
def current_user
if session[:user_id]
User.find(session[:user_id])
else
GuestUser.new(session)
end
end
We add a #name
attribute to GuestUser
.
class GuestUser
# ...
def name
"Anonymous"
end
end
This nicely simplifies the greeting code.
def greeting
"Hello, #{current_user.name}, how are you today?"
end
In the case where we render either “Log in” or “Log out” buttons, we can’t get rid of the conditional completely. But what we can do is add an #authenticated?
predicate method to both User
and GuestUser
.
class User
def authenticated?
true
end
# ...
end
class GuestUser
# ...
def authenticated?
false
end
end
By using this predicate method in the code for rendering the button, we end up with code that states its intent a little better.
if current_user.authenticated?
render_logout_button
else
render_login_button
end
Next let’s take a look at the case where we check if the user has admin privileges. We add an implementation of #has_role?
to GuestUser
. Since an anonymous user has no special privileges, we make it return false
for any role given.
class GuestUser
# ...
def has_role?(role)
false
end
end
This simplifies the role-checking code. No more check to see if the current user exists.
if current_user.has_role?(:admin)
render_admin_panel
end
Now what about the code for showing different listings to different people? We implement a #visible_listings
method on GuestUser
which simply returns the publicly-visible result set.
class GuestUser
# ...
def visible_listings
Listing.publicly_visible
end
end
Then we can reduce the listings code to a one-liner.
@listings = current_user.visible_listings
We also implement attribute setter methods as no-ops.
class GuestUser
# ...
def last_seen_online=(time)
# NOOP
end
o end
This enables us to eliminate another conditional.
current_user.last_seen_online = Time.now
In order to implement a shopping cart for users who haven’t yet logged in, we make the GuestUser
‘s cart
attribute return an instance of the SessionCart
type that we talked about earlier.
class GuestUser
# ...
def cart
SessionCart.new(@session)
end
end
Now the code for adding an item to the cart also becomes a one-liner.
current_user.cart.add_item(some_item, 1)
What we’ve done here is identify a special case—the case where there is no logged-in user. And then we represented that special case as an object in its own right. As a result, we were able to simplify quite a bit of our code. And it’s not just simpler—it reads better too!
There’s a name for this, and unsurprisingly it is the “Special Case” pattern. It’s one application of a more broad observation: anytime a program keeps switching on the same condition over and over again, that’s a good indication that there’s a new kind of object waiting and wanting to be discovered.
That’s it for today. Happy hacking!
Cool videos, thanks!
Applying the null object pattern is relatively simple. A more interesting discussion would be when to use it and when not – advantages/disadvantages.
FYI for anybody watching the second video, if you don’t want the “double nil” check for the receiver and whether it `responds_to` the method you’re invoking, use `try!` instead of `try`.
Or if you’re using ruby v2.3+, you can use the safe navigation operator: `&.`
Do not think it’s a good design. You’ve introduced god object via Guest User just to remove condition logic. App will grow and one day you will get yet another User beast. Use contracts in your code. They will ensure that user is not nil and there will be no need to double check it.
Contracts are good for rejecting unexpected, invalid inputs at API boundaries. But there was nothing unexpected about the nils in the original code. In the Special Case example, the code was written the way it was because it expected the absence of a logged-in user to be represented as a nil value.
What makes you say the the special case user is a “god object”?
Ruslan, I’m curious about what do you mean by “Use contracts in your code”. Can you give me an example? How could you improve the above code with contracts?
Probably Ruslan meant this gem https://github.com/egonSchiele/contracts.ruby
I would like to use
“`ruby
class NilClass
def method_missing(method, *args, &block)
nil
end
end
“`
to return nil to nil.*
That just like ObjectC or golang
That’s called a “black hole null object”. There are positives and negatives to using one. I think they are useful in certain scenarios, but I agree with a lot of other developers that making a “black hull nil” the default causes more confusion than it solves.
I explore the concept at greater length in Confident Ruby.
Thanks for the comment!
Great videos.
Regarding the Thoughtbot video and keeping 2 objects API in sync, what gem where you talking about?
Personally, I hate the huge rails active record API, I find it such a pain. It makes inspecting objects with tools such as pry pretty much useless.
I had a quick go at extracting programmer defined methods: pretty flaky. Am I missing something? Is there a better way to do this?
“`ruby
# Usage: MyActiveRecordObjet.methods_summary
def self.methods_summary
method_in_root = lambda do |meth|
meth.source_location&.first&.include?(Rails.root.to_s)
end
ims = instance_methods.select do |m|
method_in_root.call(instance_method(m))
end
cms = methods.select do |m|
method_in_root.call(method(m)) &&
!ims.include?(m)
end
{
class_methods: cms,
instance_methods: ims,
}
end
“`
Your video was great. The upcase one was too long too focused on TDD
As a dev who has sunk many an hour into combating nil values, thanks for clearly articulating the special case pattern for this!
spam
—
null / nil value is not a specific ruby problem.
It’s part of every language and we should not try to add some half-assed code on top of it to get rid of it
This example illustrates perfectly what’s wrong with all the magic of “rails-ish” gems.
1) “user.has_right?”. User model should not carry the logic of permissions. Permissions can vary in different contexts and therefore, model is independant of every context. Not mentioning the fact that it’s too much responsibility for user model
An independent permission library would expose a method
def allowed?(user, right)
return false unless user
# …
end
Hop, clean code, separation of concerns, no more problem
2) “authenticated?”. This is also purely depending on the context. It ties the model to an exclusive Controller environment when a simple rails app comes with jobs and rake tasks who are not linked to authentication.
A model should not defined how it’s considered as authenticated or not, it’s precisely the controller (or other top level environment) to define it
3) If you are bothered by “current_user ? current_user.name : ‘Guest'”, chose a better syntax way of expressing your code. The fact that you use the + operator to concatenate strings appears to me like you chose the most ineffective ruby syntax to prove your point.
“Hello #{current_user.try(:name) || ‘Guest’} ….” is making things much more readable actually.
4) This approach leads to more bugs actually. In this precise example, the problem comes from the fact that you are actually offering authenticated features to an anonymous visitor. It’s debatable in an online shopping approach, but it’s flawed by default.
What is someone tries to save your GuestUser? Do you need to implement the save method too? How? Does it raise an exception? Why is it better than nil error? Does it do nothing then? It’s even worse.
====
As conclusion i’d say that
– we must stop adding millions of methods and responsibilites to AR models, they’re already too big
– we must respect the simple logic that says “if there is nothing then there is nothing” instead of trying to make nothing something and then discover that guessing that a something actually means nothing is stupid, confusing and lead to extremely complex bugs that sometimes can not be solved without data loss.
Wow, you know a lot about what should and must be done! This seems like it deserves its own blog post, don’t you think?
You have important insights, but calling other people’s programming ideas “stupid” crosses a line of what kind of discussion I’ll support on sites I own. That style of discourse isn’t one I’m interested in promoting, even implicitly. I’ll leave this up for a couple weeks so you can copy it to a blog post of your own if you want and not lose all the work you put into it. Then I’ll remove it.
Great episode!
Very nice presentation that even teaches how to spot situations for abstraction, encapsulation, introduce right objects to simplify code and evolve domain model.