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 包成擴充可使用的方法。