Reputation: 1267
I'm trying to build a simple survey/questionnaire app. Surveys have Questions
; most questions consist of a single content field (the question itself), for which the survey taker will write in a free-text response. (There are also a couple of other fields not relevant to this discussion.) However, users can also create MultipleChoiceQuestions
or LikertQuestions
(e.g., answers on a 1 - 5 scale). (In the case of MultipleChoiceQuestions
, there will be another model called Answer
such that a MultipleChoiceQuestion
has_many Answers
). Here are my design choices, so far as I know:
1) Inherit from Question:
class Question < ActiveRecord::Base
attr_accessible :id, :content
end
class MultipleChoiceQuestion < Question
attr_accessible :type
end
class LikertQuestion < Question
attr_accessible :type, :min, :max, :label_min, label_max
end
2) Use a module/mixin with the shared attributes and methods:
module Question
@content, @id
def method1
end
end
class MultipleChoiceQuestion < ActiveRecord::Base
include Question
end
class LikertQuestion < ActiveRecord::Base
include Question
attr_accessible :type, :min, :max, :label_min, label_max
end
This seems a clear-cut case of inheritance, so I went with option 1. Since then, I can't get it to work. Single Table Inheritance seemed simple enough, so I gave MultipleChoiceQuestion
and LikertQuestion
each type:string
in their schema. Here are the schema for each (from db/schema.rb):
create_table "questions", :force => true do |t|
t.integer "parent"
t.string "type"
t.string "content"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.integer "survey_id"
end
create_table "multiple_choice_questions", :force => true do |t|
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.string "type"
end
create_table "likert_questions", :force => true do |t|
t.integer "min"
t.integer "max"
t.string "label_min"
t.string "label_max"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.string "type"
end
If I implement option 1, above, then MultipleChoiceQuestion and LikertQuestion somehow do not actually include any of their unique fields as specified in schema.rb; instead, they have only the inherited fields from Question. See console output:
1.9.3p392 :001 > Question
=> Question(id: integer, parent: integer, content: string, created_at: datetime, updated_at: datetime, survey_id: integer)
1.9.3p392 :002 > LikertQuestion
=> LikertQuestion(id: integer, parent: integer, content: string, created_at: datetime, updated_at: datetime, survey_id: integer)
1.9.3p392 :003 > MultipleChoiceQuestion
=> MultipleChoiceQuestion(id: integer, parent: integer, content: string, created_at: datetime, updated_at: datetime, survey_id: integer)
1.9.3p392 :004 > LikertQuestion.new(:min => 3)
ActiveRecord::UnknownAttributeError: unknown attribute: min
Somebody on StackOverflow said that Question should be an abstract class. But if I add
self.abstract_class = true
to Question.rb, then I get the following:
1.9.3p392 :001 > Question
=> Question(abstract)
1.9.3p392 :002 > LikertQuestion
=> LikertQuestion(id: integer, min: integer, max: integer, label_min: string, label_mid: string, label_max: string, created_at: datetime, updated_at: datetime, type: string)
1.9.3p392 :003 > MultipleChoiceQuestion
=> MultipleChoiceQuestion(id: integer, created_at: datetime, updated_at: datetime, type: string)
1.9.3p392 :004 > LikertQuestion.new(:content => "foo")
ActiveRecord::UnknownAttributeError: unknown attribute: content
LikertQuestion
and MultipleChoiceQuestion
show only their unique fields and do not inherit fields from the parent.
1) What am I missing here? Regardless of whether inheritance is the optimal solution, I must be overlooking something obvious.
2) Should I be using the module approach instead of inheritance? As I mentioned, inheritance seemed like a no-brainer: LikertQuestion
and MultipleChoiceQuestion
really are kinds of Questions
. If I use the module approach, I lose the ability to say things like survey.questions()
, survey.questions.build()
, and presumably other handy stuff. What do Rails hotshots do in this situation? I'll do whatever that is.
No posts on StackOverflow offer a very comprehensive discussion of the pros and cons of subclassing vs. mixin.
Using Ruby 1.9.3 (though thinking of switching to 2.0), Rails 3.2.3.
Upvotes: 0
Views: 783
Reputation: 29880
You are indeed missing something obvious. Do you know what STI stands for? Single Table Inheritence. You are making several tables and then trying to use STI.
You should only use STI if your tables identical or very similar (maybe 1 field of difference). It is primarily used when you want to subclass and then provide methods to differentiate behaviour. For example, maybe all users share the same attributes but some of them are admins. You can have a type
field in your users table, and then you might have something like this:
class Admin < User
def admin?
true
end
end
class NormalUser < User
def admin?
false
end
end
(this is obviously a very simple example and probably wouldn't warrant STI on it's own).
As far as abstract classes go, that is a good decision if you have several tables that should all inherit behaviour from a super class. It seems like it might make sense in your case; however, it is important to note that abstract classes do not have tables. The whole point of declaring abstract_class
as true is so that ActiveRecord won't get confused when trying to look for a table that doesn't exist. Without it, ActiveRecord will assume you are using STI and try to look for a Questions table. In your case, you do have a question table, so declaring it as an abstract class doesn't really make sense.
One other thing, you ask "Should I be using the module approach instead of inheritance?". Using modules is actually a form of inheritence in Ruby. When you include a module, it is inserted in the classes ancestor chain just like a super class would be (modules are inserted BEFORE super classes however). I do think that some form of inheritance is the correct approach. In this case, because they are both types of questions, making an abstract Question superclass makes sense to me. Because the questions don't share many of their attributes, storing them in separate tables is the best solution in my opinion. STI is not really a good practice when you have several differing fields, because it leads to a lot of null
in your database.
And to be clear about modules, I think that it is best done when several otherwise unrelated models share some form of common behaviour. One example that I've used multiple times is the idea of a Commentable
module (using ActiveSupport::Concern). Just because several models can be commented on doesn't necessarily warrant a superclass because the models are not related - they do not really descend from some sort of parent object. This is where a module makes sense. In your case, a superclass makes sense because both of your models are typed of questions, so it seems appropriate that they both descend from a generic Question
base class.
Upvotes: 3