Giới thiệu module Rails Concern

Kể từ bản Rails 4, một thư mục mặc định được tạo ra mỗi khi tạo project mới, đó là thư mục concerns. Ta sẽ tìm hiểu về module concern trong bài viết này.

But first, let’s return to Ruby’s realm

Module và included callback

Ruby cung cấp một hàm callback có tên included cho module. Hàm callback này sẽ được gọi mỗi khi module được included vào một module hoặc class khác. Cách dùng included rất đơn giản, ví dụ sau được trích từ document của Ruby

# test.rb
module A
  def A.included(mod)
    puts "#{self} included in #{mod}"
  end
end

module Enumerable
  include A
end
#

Khi chạy file test.rb trên sẽ được kết quả:

$ ruby test.rb
A included in Enumerable

included có thể được dùng để tách các phần logic giống nhau vào một module dùng chung. Chằng hạn trong ví dụ sau ta có 2 class EntryComment, cả 2 class đều định nghĩa các hàm như posted_at và cùng gọi các helper như validates_presence_of

class Entry
  validates_presence_of :user_id

  # formated post time
  def posted_at
    created_at.strftime("%Y/%m/%d")
  end
end

class Comment
  validates_presence_of :user_id

  # formated post time
  def posted_at
    created_at.strftime("%Y/%m/%d")
  end
end

Dùng included ta dễ dàng move các logic này vào một module riêng, cụ thể như sau:

module Postable
  def self.included(base)
    base.class_eval do
      validates_presence_of :user_id
    end
  end

  def posted_at
    created_at.strftime("%Y/%m/%d")
  end
end

class Entry
  include Postable
end

class Comment
  include Postable
end

Class methods

Khi một module được include vào một class, mặc định class đó sẽ access được các instance method được định nghĩa trong module đó nhưng lại không gọi được các class method. Chẳng hạn như hàm find_by_user_id trong ví dụ sau:

module Postable
  def posted_at
    created_at.strftime("%Y/%m/%d")
  end

  def self.find_by_user_id
    # ...
  end
end

class Entry
  include Postable
end

class Comment
  include Postable
end

# Entry.new.posted_at -> OK
# Entry.find_by_user_id(1) -> NoMethodError

Một cách để workaround chính là sử dụng callback included

module Postable
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def find_by_user_id
      # ...
    end
  end
end

Trong ví dụ trên ta dùng function base của class/module để extend các class method. Sử dụng cách này, các class/module include Postable sẽ access được hàm find_by_user_id đã được định nghĩa.

Module Concern

Các ví dụ ở trên chính là cách hoạt động của module Concern. Với việc sử dụng Concern ta viết lại module Postable đơn giản như sau:

require 'active_support/concern'

module Postable
  extend ActiveSupport::Concern

  included do
    validates_presence_of :user_id
  end

  module ClassMethods
    def find_by_user_id
      # ...
    end
  end
end

class Entry
  include Postable
end

class Comment
  include Postable
end

Ngoài ra, module concern còn giúp giải quyết vấn đề dependency. Như trong ví dụ sau, module B phụ thuộc vào module A

module A
  def self.included(base)
    base.class_eval do
	  def self.method_of_module_a
	    # ...
	  end
	end
  end
end

module B
  def self.included(base)
    base.method_of_module_a
  end
end

class C
  include A
  include B
end

Ta thấy class C chỉ muốn sử dụng module B, nhưng lại phải include thêm module A(do B phụ thuộc vào hàm method_of_module_a của A). Nhưng với việc sử dụng Concern vấn đề dependency đã được giải quyết. Ta viết lại đoạn code trên như sau:

require 'active_support/concern'

module A
  extend ActiveSupport::Concern
  included do
    def self.method_of_module_a
      # ...
    end
  end
end

module B
  extend ActiveSupport::Concern
  include A

  included do
  self.method_of_module_a
  end
end

module C
  include B
end

Chi tiết về module Concern có thể tham khảo tại:

http://api.rubyonrails.org/classes/ActiveSupport/Concern.html

https://github.com/rails/rails/blob/master/activesupport/lib/active_support/concern.rb

Vinh Nguyen