Special Case
Video transcript & code
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!
Responses