Search code examples
inheritancemethodspolymorphismassociationspolymorphic-associations

How can I mix Polymorphism and Inheritance in Rails(5.0.0.1)


I've been trying to find a solution for 2 days came up with nothing yet.

I have a model called Course that has the following columns:

create_table :courses do |c|
  c.integer :member_limit
  c.string :color
  c.float :rating

  c.timestamps
end

I also have a Content model that has columns that Course benefits from but also benefits other models in my database, such as:

create_table :contents do |c|
    c.references :contentable, polymorphic: true, index: true
    c.string :title
    c.text :description
    c.text :script
    c.string :cover
    c.string :media_type
    ...
    c.integer :creator_id, index: true, foreign_key: :user_id
end    

I can't set Course < Content because I will lose the columns that Course has internally such as member_limit and so on, so I went with Polymorphic. However, I want to avoid having to call course.content.title and just write course.title but also access course.member_limit the same way and save both those fields by using course.save.

What do you recommend the best approach to be?

current structure.

Course:

class Course < ApplicationRecord
    has_one :content, as: :contentable, dependent: :destroy

    after_initialize :init

    def init
      if self.new_record?
        self.content ||= build_content    
      end
    end
end

Content:

class Content < ApplicationRecord
   belongs_to :creator, optional: true, class_name: 'User'
end

Solution

  • I think your premise is wrong in the sense that what you want to do should not be done. I explain:

    What you want, can be done via helper methods like so:

    class Course < AR
      def title
        content.title
      end
    end
    

    Which is ugly, violates every coding argument ever written, and its just bad practice. But, apart for some meta magic that could avoid having to write the helper methods one by one (notice i said write, because ultimately, they would be implemented), the end result would have to be this way. There is no other way to do it. SO my answer, don't do this.

    Understanding polymorphism is not easy. Its easy to implement but difficult to know when. You want an abstract box that contains the columns of several other resources just because. You give no reasoning behind the decision of creating "...a Content model that has columns that Course benefits from but also benefits other models in my database". I ask. why? whats the benefit? why do this? Is it working as well as expected? is it easier to use and extend? is it encapsulated? Is it dry, rest, solid, and every other acronym used these days?

    The short answer is NO. It isn't. So don't do it. Have the Course class have title. If another table also has title, so what? Do you know how many tables in the world have the column "name"? If we all wanted to implement it your way we would have something like this:

    class School < Ar
      belongs_to :name
    
      def full_name 
        name.body
      end
    end
    
    class Student < Ar
      belongs_to :name
    
      def full_name 
        name.body
      end
    end
    

    which is clearly not useful. So if it is not useful for one column, why does it become useful for more columns?

    Conclusion:

    1. Polymorphism not used to re-use columns, never.
    2. STI is used for re-using columns (sort of)
    3. Polymorphism can be understood as something that can be attached to multiple resources. Such as: Notifications, logs, addresses... Basically say Course has Address and also Student has an Address. Both Student and Course are Addressable. BUT NEVER, NEVER, NEVER say Content is A TYPE OF Address. This is just not true in any way, shape or form.
    4. In STI the resource BECOMES a type of the parent so in STI, Course would be A TYPE OF Content. Also Student would be A TYPE OF Content. BUT NEVER, NEVER, NEVER Say Course HAS content. This is just not true in any way, shape or form.

    In STI, The Content table would have had ALL the columns of Course PLUS ALL the columns of any other table that wanted to be a course. This is good practice for resources that essentially, in data are the same or almost (seems a lot what you describe) and you don't want to repeat code in all children classes. All STI can be implemented without STI by repeating the parent class code in each child class.

    So, short answer (a bit late no?). No, you can't do what you want. You can either

    1. Just repeat the columns in each resource (preferable way for a first implementation, easy, fast and standard)
    2. Use STI and ONE TABLE to represent all resources that would have "shared" the content information
    3. Use polymorphism and use helper methods to do what you want. But don't forget accessors are not the only ones. Think about methods such as def title=. Are you gonna re-implement all of them?

    Sorry for the length, just seen this mistake too many times.