ruby

An Introduction to Polymorphism in Ruby on Rails

Jesse McDermott

Jesse McDermott on

An Introduction to Polymorphism in Ruby on Rails

If you have ever spent time building an Object-Oriented Program (OOP), you have likely used polymorphism in your application or, at the very least, heard the term.

It’s the kind of word you’d expect to see in a science or computer science textbook. You may have spent time researching polymorphism and even implemented it in your application without clearly understanding the concept.

This article will give you a greater understanding of polymorphism, specifically in Ruby on Rails. To accomplish this, we’ll dive into:

  • The use of polymorphism in the real world
  • Polymorphism in programming by way of OOP
  • How you can incorporate it into your Rails application to help maintain high-quality code

Let's get going!

Polymorphism in the Real World

There are several ways to define polymorphism in different contexts. A useful definition, regardless of context, is 'an object's ability to display more than one form'. In fact, if we break down the word itself, poly means ‘many’ and morph means ‘form’.

In the real world, a basic example of this definition could be a woman who is also a police officer, sister, someone’s child, someone’s mother, etc. Each role determines her behavior and contributes to the person she is.

Polymorphism and Genetics

Outside computer programming, polymorphism is a term commonly associated with biology and genetics. In this context, polymorphism is more specifically defined as genetic variations that result in several distinct forms or types of individuals within a species.

Think of jaguars. Jaguars can have multiple gene variations, which can affect their fur coloring. Most jaguars have tawny coloring with black circles. However, they can have lighter or darker circles due to an altered gene, and some can have black fur coloring.

Different pigmentation within the same species of birds is another example of polymorphism. Consider the Gouldian finch, which has obvious distinctions in its coloring between individuals.

Monomorphism vs. Polymorphism

If we turn our attention to monomorphism, we can further understand polymorphism. Sticking with biology, monomorphism can be defined as 'a species with just one form', that maintains that same form during the various phases of its development.

Penguins are monomorphic. It is difficult, even for experts, to distinguish between the sexes. Gene differences in a penguin are minimal. Therefore, the physical attributes of penguins are almost indistinguishable, especially in terms of their size and black and white coloring.

Behavioral cues are often the easiest way to discern between the sexes in monomorphic species.

Let's turn our attention to polymorphism in programming, specifically.

Polymorphism in OOP

If we consider our initial definition of polymorphism — the ability of an object to display more than one form — we can seamlessly relate it to OOP.

In OOP, we can use the same method to produce different results by passing in separate objects. We could use conditionals to achieve this. However, this can create chunky code and may veer us away from DRY principles. Polymorphism is essential in creating clean and logical OOP applications.

Let's look at two examples of how polymorphism can be implemented in an OOP language like Ruby: through inheritance and duck-typing.

Polymorphism and Inheritance in Ruby

Inheritance is where a child class inherits the properties of a parent class.

Below is an example of how we can implement polymorphism with inheritance:

class Instrument
  def instrument_example
    puts "Saxophone"
  end
end
 
class Stringed < Instrument
  def instrument_example
    puts "Guitar"
  end
end
 
class Percussion < Instrument
  def instrument_example
    puts "Drums"
  end
end
 
all_instruments = [Instrument.new, Stringed.new, Percussion.new]
 
all_instruments.each do |instrument|
  instrument.instrument_example
end
 
# Output
 
# Saxophone
# Guitar
# Drums

The above code has two child classes — Stringed and Percussion — inherited from the parent class Instrument. This example is polymorphic, as we are calling a method: instrument_example — and it outputs multiple forms: Saxophone, Guitar, and Drums.

This example of achieving polymorphism through inheritance is essentially overriding a method, but helps provide a clearer understanding of polymorphism in an OOP language.

Duck-Typing and Polymorphism in Ruby

A more practical example of polymorphism in OOP is through duck-typing, as referenced below.

class Guitar
  def brand
    'Gibson'
  end
end
 
  class Drums
    def brand
      'Pearl'
    end
  end
 
  class Bass
    def brand
      'Fender'
    end
  end
 
  class Keyboard
    def brand
      'Casio'
    end
  end
 
  all_instruments = [Guitar.new, Drums.new, Bass.new, Keyboard.new]
 
  all_instruments.each do |instrument|
    puts instrument.brand
  end
  # Output
 
  # Gibson
  # Pearl
  # Fender
  # Casio

Though each class method is named brand, we don't override the method (unlike in polymorphic inheritance). Instead of inheriting from a parent class, here we have four independent classes, each with its own method. Duck-typing is useful, as we can just iterate through the classes to get each method's output (as opposed to calling each method separately).

Again, duck-typing is polymorphic as we call a method — brand — and generate an output that takes multiple forms: Gibson, Pearl, Fender, and Casio. Of course, duck-typing and polymorphism aren’t essential in producing this outcome. However, it’s very useful to implement clean and logical code.

Polymorphism in Ruby on Rails

Polymorphism works well in Ruby on Rails as an Active Record association. If models essentially do the same thing, we can turn them into one single model to create a polymorphic relationship.

Sticking with the instrument theme, let's consider an application where users can post, comment, and review instruments. Examine the Entity-Relationship Diagram (ERD) below:

first erd - no Polymorphic association

In this example, we have an ERD for an application where a user can post an instrument with its details. A user can also provide a comment about that posted instrument.

Other users can then provide a rating of the instrument and rate comments to determine their usefulness or validity. These Active Record associations work just fine and serve the purpose of our application.

What if we wanted to add other associations to our application? We would need to add and repeat duplicate associations.

For example, if we wanted to add a user_rating model to rate the trustworthiness of a user, we would need to create a separate table with its own associations. This would mean adding a new relationship between the user and user_rating models. The ERD would then look something like this:

second erd - example of adding another model with no Polymorphic association

Now we have three models essentially doing the same thing: rating an object, but in different contexts. These associations are ripe for a polymorphic association.

Let's take a look at the ERD with the rating models as polymorphic:

third erd - example of a Polymorphic association

As we have a rating column, I named the model reviews as opposed to ratings to avoid confusion. Here, the non-review models still have associations with the other models, but the separate rating models have been merged into a single review model.

reviewable_type and reviewable_id now take on the same role as the separate rating models by representing which model the review is associated with.

The reviewable_type column stores the model class name (user, instrument_post, or comment), and the reviewable_id stores the corresponding ID of that model.

We can now use these two columns to link the rating integer with a specific user, post, or comment via Active Record queries and/or conditional statements. The foreign key user_id remains in the review model, as this allows us to track which user left a review.

Right now, the term ‘-able’ in our polymorphic model may seem strange, but its purpose will soon be made clear when we do some Rails magic.

The review model is considered polymorphic as we have one model or object that can represent and take on multiple forms: user, comment, and instrument post reviews.

Implementing Polymorphism in Ruby on Rails

Time to implement polymorphism in a Rails application! If we act as though we have already created our user, instrument_post, and comment models, we can get started on incorporating our polymorphic model: reviews.

Firstly, create a table and generate the model from the terminal, like so:

rails g model Review user:belongs_to reviewable:references{polymorphic}

This builds the migration file:

class CreateReviews < ActiveRecord::Migration[7.0]
  def change
    create_table :reviews do |t|
      t.belongs_to :user, null: false, foreign_key: true
      t.references :reviewable, polymorphic: true, null: false
      t.integer :rating
 
      t.timestamps
    end
  end
end

The schema.rb file updates after the migration is run. The polymorphic option transforms the reviewable column into the reviewable_type and reviewable_id columns:

  create_table "reviews", force: :cascade do |t|
    t.integer "user_id", null: false
    t.string "reviewable_type", null: false
    t.integer "reviewable_id", null: false
    t.integer "rating"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["reviewable_type", "reviewable_id"], name: "index_reviews_on_reviewable"
    t.index ["user_id"], name: "index_reviews_on_user_id"
  end
# app/models/review.rb
 
class Review < ApplicationRecord
  belongs_to :user
  belongs_to :reviewable, polymorphic: true
end

Remember when we mentioned the term ‘-able’ above? It is a Rails naming convention for designating our polymorphic association, giving us the ability to make a user, instrument post, and comment 'reviewable'.

For this Rails magic to work, we need to ensure that our other models are associated correctly with our polymorphic model.

# app/models/user.rb
 
class User < ApplicationRecord
  has_many :instrument_posts
  has_many :comments
  # alias association for user who submitted the review
  has_many :submitted_reviews, class_name: "Review", foreign_key: :user_id
  # association for user, instrument_post and comment that has the review
  has_many :reviews, as: :reviewable
end
# app/models/instrument_post.rb
 
class InstrumentPost < ApplicationRecord
  belongs_to :user
  has_many :reviews, as: :reviewable
end
# app/models/comment.rb
 
class Comment < ApplicationRecord
  belongs_to :user
  has_many :reviews, as: :reviewable
end

The user, instrument_post, and comment models can now be reviewed and given ratings.

If we have already created at least two users, a comment, and an instrument post, we can then create and access the reviews through various ways with Active Record Queries, like so:

# A user (User 2) leaving a review for another user (User 1)
# The reviewable_id is the id of the user to which the review is given
# The user_id is the id of the user who created the user review
user_1 = User.first
user_1.reviews.create(user_id: 2, rating: 2)
Review.where(reviewable_type: "User").first # id: 1, user_id: 2, reviewable_type: "User", reviewable_id: 1, rating: 2
# or
user_1.reviews.first # id: 1, user_id: 2, reviewable_type: "User", reviewable_id: 1, rating: 2
# User 2 reviewing a instrument posted by User 1
# The reviewable_id is the id of the instrument_post
# The user_id is the id of the user who created the instrument_post review
post = InstrumentPost.first
post.reviews.create(user_id: 2, rating: 4)
post.reviews.first # id: 2, user_id: 2, reviewable_type: "InstrumentPost", reviewable_id: 1, rating: 4
post.reviews.first.reviewable_type # "InstrumentPost"
# User 2 reviewing a comment created by User 1
# The reviewable_id is the id of the comment
# The user_id is the id of the user who created the comment review
comment = Comment.first
comment.reviews.create(user_id: 2, rating: 5)
comment.reviews.first # id: 3, user_id: 2, reviewable_type: "Comment", reviewable_id: 1, rating: 5
comment.reviews.first.rating # 5
 
# We can then find the user that created the reviewed comment by associating the value of the reviewable_id to the comment id
Comment.where(id: 1) # id: 1, user_id: 1, content: "Comment created by user id 1"
# In this scenario the comment id and user_id just happen to be the same
user_1 = User.first
user_2 = User.last
 
# Reviews User 1 has given (none)
user_1.submitted_reviews # []
 
# Reviews User 2 has given
user_2.submitted_reviews
# id: 1, user_id: 2, reviewable_type: "User", reviewable_id: 1, rating: 2
# id: 2, user_id: 2, reviewable_type: "InstrumentPost", reviewable_id: 1, rating: 4
# id: 3, user_id: 2, reviewable_type: "Comment", reviewable_id: 1, rating: 5
 
# Counting the total amount of reviews User 2 has given
user_2.submitted_reviews.count # 3

There are many other ways to interact with the reviews model, depending on what data you need to render. It is important to share how the parent models can create a review. As long as the review is associated with a user and a reviewable model, Active Record automatically links the reviewable_id and reviewable_type with the associated model.

Without polymorphism in our Rails examples, there would be many more tables, unnecessary duplicate columns, belongs_to, and has_many associations in our models. Polymorphism has lessened the need to join tables, permitting easier and quicker Active Record queries and associations.

Wrap Up: Use Polymorphism for Clean and Logical Ruby Code

In this post, we explored polymorphism in two distinct environments: biology and Ruby programming. In both cases, polymorphism is the ability of an object to display more than one form.

We looked at how to implement polymorphism in Ruby through inheritance and duck-typing before diving into the uses of polymorphism in Ruby on Rails specifically.

Polymorphism can help you write clean and logical code. My goal is to help you add this essential OOP concept to your toolbelt for your current, future, and maybe even past applications.

Happy coding!

P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!

Share this article

RSS
Jesse McDermott

Jesse McDermott

Our guest author Jesse is a software developer residing in Canada and originally from New Zealand. He's inspired to help people and communities through coding. When Jesse's not coding, he's playing music or out hiking with his dog Tiggy.

-> All articles by Jesse McDermott-> Become an AppSignal author

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps