[Rails Notes] Rails Concerns, and an Example of Wrapping Polymorphic Association
Sometimes, we would want to DRY the code a little bit by extracting some common behaviors/functions. If the entities where these common behaviors belong are similar in essence, we may consider using a parent class. But in other cases, it’s not an easy job to find a parent class for something that share common behavior, say, “fly”, but are totally different in essence – like bird, kite and plane.
In Java, a common practice is to use interface
. Basically, defining common functions (optionally with a default implementation) in the interface and implement them in concrete classes. In Ruby, Module
is playing the similar role. Moreover, Rails provide a nice wrapper for that – ActiveSupport::Concern
.
Some notes on using Concern
:
- Purpose: extract common methods/behaviors.
- Conditions: entities, who have those common behaviors, should be different in nature
- It should be domain independent, i.e. not coupled with any class that include itself.
An example use of Rails Concern – polymorphic associations
Scenario:
Users can review multiple types of things – Document
, Restaurant
, etc. On the other hand, Document
, Restaurant
, etc. receives reviews from multiple users, and has methods – #positive?
and #score
– to return aggregated reviews performance.
We want a has_many ... through: :reviews
relations between User
and “reviewable”s, such as Document
and Restaurant
.
We certainly can do
has_many :reviews
has_many :documents, through: :reviews
has_many :restaurants, through: :reviews
...
in user.rb
, and
has_many :reviews, dependent: :destroy
has_many :users, through: :reviews
def positive?
...
end
def score
...
end
for every “reviewable” model. But that is not DRY. We would want to build a polymorphic association, where users have many “reviewable” through Review
. Also, suppose what we care about each “reviewable” is the overall review, i.e. #positive?
and #score
, but not the exact type of each one being review.
The use of polymorphism here exemplifies one of design principles:
Depend on abstractions, not concrete classes.
where Reviewable
Concern acts as the abstraction layer.
Solution
In Review
model (which is the “join table”):
class Review < ActiveRecord::Base
belongs_to :user
belongs_to :reviewable, polymorphic: :true
...
end
In Reviewable
concern – set up common associations, extract abstract methods for the reviewables:
module Reviewable
extend ActiveSupport::Concern
included do
has_many :reviews, as: :reviewable, dependent: :destroy
has_many :users, through: :reviews
end
def score
raise NotImplementedError
end
def positive?
raise NotImplementedError
end
end
Then, in reviewable classes, include
the Reviewable
concern, and override methods:
class Document < ActiveRecord::Base
include Reviewable
...
end
class Restaurant < ActiveRecord::Base
include Reviewable
...
end
Lastly on the users side:
class User < ActiveRecord::Base
has_many :reviews
has_many :documents, through: :reviews, source: :reviewable, source_type: :'Document'
has_many :restaurants, through: :reviews, source: :reviewable, source_type: :'Restaurant'
...
end
Another thing that might be a little tricky, is the schema of Review
DB table. Migration:
class CreateReviews < ActiveRecord::Migration
def change
create_table :reviews do |t|
t.integer :user_id
t.integer :reviewable_id
t.string :reviewable_type
t.timestamps null: false
end
end
end
Pros & Cons in this example
By wrapping polymorphic association in Rails Concern, it DRY the code on the reviewables side. However, it does not reduce the manual work on the users side. Every time a new reviewable is associated, a new has_many ...
line should be manually added. Also, the abstraction is on application layer. In other words, it does not add a DB table, and thus does not add the functionality of fetching all associated reviewables (i.e. user.reviewables
) automatically.
Related links & references
- Rails Doc –
ActiveSupport::Concern
- Ruby Doc –
Module
- Polymorphic has_many through Associations in Ruby on Rails