views:

92

answers:

3

I have a large Rails app that has 34 different types of Users through Single Table Inheritance. When the app was initially designed, it was assumed that the User would have different behaviors based on type. This assumption was wrong, so I'm looking to refactor.

The question is how would you refactor out the User STI?

  • Add a ton of boolean attributes to User (ie. User#is_employee?, User.is_contractor?)
  • Rename User#type to User#user_type and just do string matching based on the field
  • Some horrible lookup table solution
  • Something I'm missing...

Just to clarify, a User needs a 'type' for ACL reasons, but with STI they're really just empty models and STI causes issues with specing, general pain in the ass, etc.

A: 

Are all 34 models empty?

If they are you could do:

self.inheritance_column = nil

At the top of your User model, that way you can just check type:

if @user.type == "Employee"
  # do something
end
Ryan Bigg
Interesting and simple...
Matt Darby
A: 

It's tough to answer the question without knowing in what way your 'ACL reasons' will be using the type, but...

In my experience I have always gone with User#user_type. It always turned out for me that the cases were rare enough that this was good. Though I have only ever had a few user types. Another option (which could be what you were alluding to with your second option) would be to use method_missing to handle the string matching, and allow it to behave like option 1.

For example:

def method_missing(method, *args, &block)
 if(method.to_s.starts_with?('is_'))
  self.user_type == method.to_s.gsub("is_", "").gsub("?", "")
 end
end

this would return true for is_contractor? if the user_type is contractor.

Note that the snippet provided is untested, and that the chained gsub's are pretty awful looking. I'm sure this can be written as a nice little regular expression or in some other clever way.

+3  A: 

It sounds like what you really need are roles rather than user types. I've found that this is usually the case when you end up with many user types, is because some users have more than one role.

A simple ACL model is: User has many roles, each role has many permissions. The permission set for a user is the set of all permissions for all roles the user belongs to.

In more complicated cases a permission is often a calculated property rather than a row in a database, for the simple reason that permissions are some times target and time based (!).

@user.can_view_payroll_info_for?(@employee, 2.years.ago) ==> true
@user.can_view_payroll_info_for?(@employee, Time.now) ==> false

Most of the time though, a role is as high resolution as you need to get:

@john.has_role(:content_author) ==> true
@john.has_role(:moderator) ==> true

@benny.has_role(:content_author) ==> true
@benny.has_role(:moderator) ==> false

@michael.has_role(:moderator) ==> true
@michael.has_role(:content_author) ==> false

You can implement it as simply as a comma separated (validated) string 'roles' column for the user or a join table if you want roles to be normalized.

Redbeard