Pergunta

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.

Foi útil?

Solução

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.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top