Rails4 Patterns III: Concerns
Concerns 是 Rails 拿來做模組化的一個方式,在 Rails4 之後也正式在 app/models/ 內有了 concerns 這麼一個資料夾的一席之地,也是一種 Rails Convention。
把重複的 Model 程式碼搬到 Model Concerns 內##
留言功能在臉書不斷的演進下,似乎已經成為現在網站的一個基本盤功能,什麼內容都可以討論一下。Rails4 Patterns 裡面拿這個當做範例,覺得蠻適合的。
# app/models/post.rb
class Post < ActiveRecord::Base
has_many :comments, as: :commentable
def comments_by_user(id) # 重複了
comments.where(user_id: id)
end
end
# app/models/image.rb
class Image < ActiveRecord::Base
has_many :comments, as: :commentable
def comments_by_user(id) # 重複了
comments.where(user_id: id)
end
end
# app/models/comment.rb
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
end
以上方來說利用了 Rails 內的 Polymorphic Association 的特性來做到文章跟圖片都可以留言,但也因如此,造成文章與圖片的 Model 出現重複程式碼在所難免。
若是包成 Concern 的方式來做,不只可以重複使用、避免重複程式碼,更可做到模組化。
# app/models/concerns/commentable.rb
module Commentable
def self.included(base) # 這邊還可以更優化
base.class_eval do
has_many :comments, as: :commentable
end
end
def comments_by_user(id)
comments.where(user_id: id)
end
end
# app/models/post.rb
class Post < ActiveRecord::Base
include Commentable
end
# app/models/image.rb
class Image < ActiveRecord::Base
include Commentable
end
純以 Ruby 寫法來說這樣包成 Module 完全沒有問題,但若善用 Rails 內建的 Library 會更好
# app/models/concerns/commentable.rb
module Commentable
extend ActiveSupport::Concern # 提供了 included 等的方法,更方便將 Module 包裝成 ActiveRecord 的擴充
included do
has_many :comments, as: :commentable
end
module ClassMethods
# 因為 ActiveSupport::Concern 的關係,讓這裡面的 ClassMethods 可以直接變成引用的 class 內的 class method
# 像是 Image.upvote(@comment)
def upvote(comment)
# ...
end
end
def comments_by_user(id)
comments.where(user_id: id)
end
end
Controller 一樣也有 Concerns##
相似性質的 Resource 操作也容易出現重複程式碼
# app/controllers/images_controller.rb
class ImagesController < ApplicationController
def show
@image = Image.find(params[:id])
file_name = File.basename(@image.path) # 重複了
@thumbnail = "/thumbs/#{file_name}"
end
end
# app/controllers/videos_controller.rb
class VideosController < ApplicationController
def show
@video = Video.find(params[:id])
file_name = File.basename(@video.path) # 重複了
@thumbnail = "/thumbs/#{file_name}"
end
end
包成 Controller Concern
# app/controllers/concerns/previewable.rb
module Previewable
def thumbnail(attachment)
file_name = File.basename(attachment.path)
"/thumbs/#{file_name}"
end
end
# app/controllers/images_controller.rb
class ImagesController < ApplicationController
include Previewable
def show
@image = Image.find(params[:id])
@thumbnail = thumbnail(@image)
end
end
# app/controllers/videos_controller.rb
class VideosController < ApplicationController
include Previewable
def show
@video = Video.find(params[:id])
@thumbnail = thumbnail(@video)
end
end
從這些範例與程式碼演進過程,也可以知道一個小知識。就是 concerns 跟 ActiveSupport::Concern 是不一樣的,concerns 是指 Rails4 裡面將重複程式碼模組化的資料夾位置,ActiveSupport::Concern 是從 Rails3 開始,當做是 module 包成擴充可使用的方法。