Skip to content

Instantly share code, notes, and snippets.

@tonywok
Last active August 29, 2015 14:13
Show Gist options
  • Save tonywok/d4395f242e8b29f3f126 to your computer and use it in GitHub Desktop.
Save tonywok/d4395f242e8b29f3f126 to your computer and use it in GitHub Desktop.
More Flexible User Models

More Flexible Users

Applications are built for users. We give them distinguishing properties, connect them to other entities, respond to their actions, and make various decisions based on any combination of those things.

In rails, it's almost guaranteed that sitting inside of app/models/ is a class called User. I'm sure you're intimately familiar with this class. In fact, I suspect that if you were to hack together a heat-map representing your code editor, user.rb would probably be molten.

The Setup

Taking this into consideration, how would you describe the responsibility of your User model? I've noticed a few trends while working in rails:

  1. An end-user domain entity for creating meaningful data relationships
  2. Authorizing an end-user can perform an action within the system
  3. Authenticating the end-user is who they claim to be

There are plenty of posts on the various ways to achieve #1 and #2, and solutions vary from plug-and-play gems all the way to philosophical re-imaginations of how rails could work. Uncontroversially, I'd just like to state that they each come with their pros and cons, and it... wait for it... sorta just depends on what you're doing.

That said, I have a bit of a bone to pick with #3. In the rails community, at least from where I'm sitting, it seems we've decided that it's such a solved problem that you can just gem install authentication-hotness, shove it in user.rb and be on your way. In most cases, it's a one-liner that pulls in a well-tested, battle-hardened, re-usable library that has saved clients everywhere thousands upon thousands of dollars. For example:

class User < ActiveRecord::Base
  devise :database_authenticatable, :validatable
end

The practice of doing this demonstrates just how far we've come. We've taken a set of requirements and turned it into a deceptively uninteresting problem.

At this point in development, things are usually going great. You're pumping out features left and right. You want posts? You got it. Followers? Sure. I'm sure if you've been developing rails apps, you've probably seen code that resembles this:

class User < ActiveRecord::Base
  devise :database_authenticable, :validatable
  
  has_many :posts, inverse_of: :user, dependent: :destroy
  has_many :comments, inverse_of: :user, dependent: :destroy
  
  def crazy_business_logic!(foo)
    dont_do_this($1) unless foo =~ /(wat)/
  end
end

The Problem

Then, undoubtedly, something comes down the pipe that tests the flexibility of your beloved User. For example, let's say you are asked to implement some variation of user invitations. Sounds easy enough, right? I've seen this handled in a few ways.

  1. Create a user with either garbage or omitted credentials
  2. Create a table for invited users and hydrate them into valid users upon accepting an invitation

You can pull of #1 in a couple ways. On one hand, you can generate a random password for them and require them to create their own during sign up. Ew. On the other, you can leave the password blank and complicate your validations -- likely dipping into external library code. Using either strategy, you now have a bunch of "users" in your system that may never even become "real".

At this point alternative #2 is starting to look more appealing. Instead, you'll separate your invitation logic into it's own model. Take a moment to unravel your SRP flag and plant it into the ground in the most epic of fashion. A few moments pass, and you start to wonder... What happens when your invited user has complex relationships with other domain entities within the system? Now you have to somehow serialize that information into the invited user so that it can be rehydrated and attached to the user once she/he accepts the invitation. I don't know about you, but I can already hear the thunderous keystrokes of future developers furiously typing git blame.

The Turn

Let's take a step back. What could we have done differently to make our lives easier?

Remember day one when we installed that amazing gem that fast-tracked us to real feature development? I'm going to make the argument that, if you're not careful, it can silently hamstring your project. It's not the libraries fault, it's ours. By hastily putting devise in our user model, we've subtly coupled together our user's domain with its' identity. This goes unnoticed for some time, but eventually it catches up with us -- sometimes even canceling out a good portion of the time we saved up front.

So let's take a shot at righting our wrong. We'll setup our user like before, except now we've moved all authentication responsibility to a class called Identity.

class User < ActiveRecord::Base
  has_one :identity, inverse_of: :user, dependent: :destroy
  
  # relationships, validations and business logic...
end

Now our identity class can handle all of the functionality that was previously crammed into User.

class Identity < ActiveRecord::Base
  belongs_to :user, inverse_of: :identity
  
  # BYO authentication mechanism (devise, has_secure_password & friends, etc)
end

With this refactored model, we can simply create a User with any domain relationships we see fit without having to hack around any authentication related constraints. Once that's done, we send out an email containing a link to accept the invitation. Upon clicking, we can find our user by token, and prompt them to create an identity to house their credentials. I tend to use rails MessageVerifier` for this, which I plan on covering in a future post.

The benefit of modeling your users this way doesn't stop at invitations. You now open yourself up to a handful of other simple implementations of historically hairy features. A few that come to mind include simple authentication setup for tests, admin user impersonation, and users with multiple identities. Look for these in upcoming blog posts.

@felixflores
Copy link

I would consider moving "The Problem" section to the top of the blog, or maybe combine the "The Setup" with the "The Problem" with a more immediate emphasis on the "The Problem".

@felixflores
Copy link

Take a moment to unravel your SRP flag and plant it into the ground in the most epic of fashion.

SRP is a familiar acronym for most of us, but it might confused some readers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment