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:
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:
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:
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!
Top comments (0)