9

I am getting lots of push back from Rails because I have subclassed User into many different subclasses. In my application, not all users are equal. There's actually a lot of model objects and not every User type has access to them.

I also need a way to do polymorphic behaviour. For example, many methods will behave differently depending on the type. Isn't that what polymorphism is for?

But the thing is, I am always getting push back from Rails. The defaults - especially the way forms submit to parameter hashes - seem to work like non-subclassed models. Links and parameter hashes are just two ways in which the defaults really bite you.

What is the "right" way to handle complex logic for different types of Users in Rails? In Java, the subclassing model works - you don't have to go through hoops to get it to work the way you want to. But in Rails, it's hard to get subclasses to work with REST conventions, it punishes you when you forget to include :as => :user, or it punishes you when you put a subclassed object in links, such as edit_user_path(@user) <- bad idea!

There's one other area which is also very hard to deal with. Let's say I have a Company model and it has many Users. These users can be directors, instructors, trainees, etc - all different subclasses.

When we create the account, we might want to use accepts_nested_attributes_for :users. However, if we use this, we can't specify the classes that it creates. Bloody hell!

It seems like everything in Rails is designed to not want you to subclass your models. If you don't subclass, everything 'just works'. But if you subclass, you are in for hell.

What is the solution?

Fire Emblem
  • 5,961
  • 3
  • 24
  • 37

3 Answers3

3

Generally speaking, inheritance is discouraged in Ruby in favor of mixin behavior and delegation. Ruby and Rails can do that stuff but it tends to result in the push back you mentioned

Your particular example sounds like a case for delegation: have a User class which belongs to an employee (or vice versa). The type-specific behavior of that employee (eg director, instructor, etc) is all in that specific employee class. Then the user will delegate how to handle specific scenarios to the employee that it's joined with

Ari Russo
  • 273
  • 2
  • 10
  • I also agree entirely with this. It took me a while to break of the habit of always subclassing things and opting to mix in behaviour instead. Not always, but often. – d11wtq May 27 '11 at 11:37
  • @russo: So to be clear before I send a checkmark to someone, What I have is a User class that is not subclassed... but I have a delegate that IS subclassed? Then if the user is an Admin, it calls only the available associations/methods on the delegate, and likewise for other types? – Fire Emblem May 27 '11 at 14:52
  • @russo: would it not be better to have a basic ruby object called Role that is subclassed and not an active record... and this object gets instantiated whenever a User is instantiated. It can basically be a strategy/state object. Would that maybe be better? I am asking these questions because they are really important to the application and I don't want to get it wrong if I can avoid it :) – Fire Emblem May 27 '11 at 14:53
  • Almost... but NO SUBCLASSES! :) what you're calling Class Role I would call Module EmployeeBehavior or UserBehavior and then mix that in to the delegate class using "include". That module would contain only the 100% shared behavior for the various delegates. Then each delegate class would contain whatever other behavior is peculiar to it. I understand completely why you're asking! I definitely learned this one the hard way. – Ari Russo May 27 '11 at 15:35
  • @russo Okay, I might understand... sort of. So when the `User` object is instantiated, we look to see which roles it has and dynamically include the modules? Or do we just include all the modules right from the get go? – Fire Emblem May 27 '11 at 15:59
  • Well, there's a number of ways you can handle that. You could pass the User object constructor the Director or Trainee class. Or you could have a builder that puts together the User object with all of its various delegate behaviors. I would just experiment a little bit and it'll start to sink in – Ari Russo May 27 '11 at 16:28
  • @russo: Ok, thanks ;) I'll try and and refactor out of the subclasses little by little and we'll see how it goes – Fire Emblem May 27 '11 at 19:58
  • Ari, I like your suggestions, but I don't understand how you're referring to "each delegate class" at the same time you're suggesting no subclasses. Wouldn't each delegate class be a subclass of some parent class? – Feech Aug 17 '12 at 14:58
1

It's basically about "say what you mean",

the framework has no way of knowing when you say redirect_to @user if you meant the generic user form or the specialized employee user form.

This leads to a lot of redirect_to @user.becomes(User) which you're free to DRY up

def to_base_class
  becomes User
end
alias_method :to_b, :to_base_class

and write redirect_to @user.to_b when your intention is to redirect to User and not Employee resource

Basically, elegant syntax like redirect_to @user represents very deep coupling between model and view/controller and as you make the model and view/controller logics more complex cracks due to this coupling will begin to show and some additional effort in domain separation will need to be made or a bit more code will need to be written.

Rails isn't punishing you for using OOP, you're experiencing increased complexity of model <-> view <-> controller relationship:

once upon a time you model was your view and vice versa, now you have two model classes mapped to two view classes and if you want to use one view for the other model you will have to say it

bbozo
  • 7,075
  • 3
  • 30
  • 56
1

Here's a trick I figured out. Don't assume it's intended behaviour though:

class UserSubclass < User

  def self.model_name
    User.model_name
  end

end

Basically all models (that derive from ActiveModel) identify themselves, by default, based on the concrete class name. That's done through the class method #model_name (it returns an instance of ActiveModel::Name with self as the parameter. Overriding it to return a specific class puts Rails on the right track. This way you keep that logic in your model and out of your templates.

d11wtq
  • 34,788
  • 19
  • 120
  • 195
  • @d11wtq: That's a neat trick. How do you deal with things like `accepts_nested_attributes_for :users` and not being able to set the sub-class it creates? – Fire Emblem May 26 '11 at 23:59
  • I haven't had to do that, so not really sure. You'd have to dive into the source and figure out what Rails is actually doing, then override as you see fit. Unfortunately (feature, not bug... apparently), Rails really only document ways of doing things using what they consider to be good application design, and that might not necessarily align with other peoples' way of thinking. Usually you can override behaviour though, and Ruby as a language lends itself to that. – d11wtq May 27 '11 at 00:04
  • @d11wtq: I have a feeling it doesn't want me to use subclasses period. I don't know how you have an OO language and not allow it. Frankly, I am starting to think that I should have a User class with every has_many/belongs_to association for all user roles in it... and then just stop people from adding to them if they are not supposed to. This is probably what Rails wants. It's dumb though because users are going to have so many associations they won't/can't even use. This solution will also have me checking types of users to decide what to do for various methods since there is no polymorphism – Fire Emblem May 27 '11 at 02:06
  • If you're fighting against ActiveRecord, you may have more fun with DataMapper. I actually use DM myself, because I find ActiveRecord to be too restrictive, especially given that I have to work with existing databases, not newly created ones. http://datamapper.org/. It is broken into lots of smaller projects, so you have to add quite a few `dm-*` gems to your Gemfile, but it's all easy. `dm-rails` is the project you want for Rails integration. – d11wtq May 27 '11 at 11:34
  • Also, if you do like the look of DataMapper and decide to give it a go, bear in mind that many gem developers assume you're using ActiveRecord. This will get better over time, since Rails 3 is still quite new and before Rails 3 it wasn't really easy to swap out ActiveRecord like this. Many of the mainstream gems expressly have support for both DataMapper and ActiveRecord, but not all of the smaller ones do. – d11wtq May 27 '11 at 11:35
  • Sorry, last one, direct link to the bit of the documentation you're likely to be interested in: http://datamapper.org/docs/misc – d11wtq May 27 '11 at 11:38