Giới thiệu module ActiveSupport Concerns trong Rails

Mở đầu

Kể từ Rails 4, một thư mục mặc định với tên là concerns được tạo ra mỗi khi tạo một project mới. Concern thực chất là các đoạn code được tách nhỏ ra cho phép chúng ta có thể tổ chức code một cách mạch lạc, “sạch sẽ” hơn. Tính năng này đã xuất hiện từ rất lâu trước khi phiên bản Rails 4 ra đời, tuy nhiên tới phiên bản này thì mọi thứ đã được chuẩn bị sẵn sàng cho chúng ta sử dụng. Trong bài viết này, chúng ta sẽ tìm hiểu về module concern và chỉ tập trung vào phiên bản Rails 4.0, tuy nhiên các phiên bản thấp hơn vẫn có thể áp dụng tương tự.

Để hiểu rõ bản chất của module Concern, trước hết ta sẽ cùng nhau ôn lại một số kiến thức cơ bản về Ruby nhé. (ok)

Module, included callback

MODULE

Ruby module cho phép chúng ta gom các methods lại thành một nhóm và sau đó các methods này có thể được sử dụng bằng cách include module chứa chúng vào trong bất kỳ module/class nào khác.

Vì ta không thể khởi tạo trực tiếp đối tượng của lớp Module, vì vậy muốn sử dụng được các method trong module, ta cần include module vào trong class thông qua method include và sử dụng đối tượng của class để gọi ra các method trong module. Ví dụ sau minh họa cho cách sử dụng của module:

Ví dụ 1.1:

# ../model/concerns/warm_up.rb
module WarmUp
  def push_ups
    "Phew, I need a break!"
  end
end

# ../model/football.rb
class Football < ActiveRecord::Base
  include WarmUp
  has_many :players
  validates :number_of_player, presence: true

  def free_kick
    "I'm pratising free kick."
  end
end

# ../model/kungfu.rb
class Kungfu < ActiveRecord::Base
  include WarmUp
  has_many :players
  validates :number_of_player, presence: true

  def kick
    "I'm kicking to the sandbag"
  end
end

Trong ví dụ trên, 2 class Football và Kungfu đều định nghĩa method push_ups với nội dung giống nhau, vì vậy ta có thể tạo ra module Warmup để có thể tái sử dụng method này .Kết quả chạy như sau:

$ rails c
$ puts Football.new.push_ups
Phew, I need a break!
$ puts Kungfu.new.push_ups
Phew, I need a break!

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. Ví dụ sau sẽ minh họa cho cách dùng của hàm callback này.

Ví dụ 1.2:

# test_include .rb
module Foo
  def self.included klass
    puts "#{self} is included in #{klass}"
  end
end

module Bar
  include Foo
end

class Lorem
  include Foo
end

Khi chạy file test sẽ trả về kết quả như sau

$ ruby test_include.rb
Foo is included in Bar
Foo is included in Lorem

VẤN ĐỀ GẶP PHẢI KHI SỬ DỤNG MODULE

Hạn chế của việc một class include một module đó là class đó chỉ có thể truy cập các instance methods của module mà không thể truy cập tới các class methods. Xét ví dụ sau:

Ví dụ 1.3:

# ../model/concerns/warm_up.rb
module WarmUp
  def push_ups
    #..
  end

  class << self
    def run_5_round
      #..
    end
  end
end

# ../model/football.rb
class Football < ActiveRecord::Base
  include WarmUp
end
$ rails c
$ puts Football.new.push_ups #OK
$ puts Football.run_5_round #NoMethodError

Như đã thấy, ta nhận được lỗi NoMethodError khi cố gắng truy cập class method của module WarmUp từ class Football. Một cách giải quyết vấn đề này là ta nhóm các class methods trong một module và extend nó trong included callback, đồng thời trong callback này, ta có thể viết các method về validates, quan hệ, scope.. để các model có thể tái sử dụng :

Ví dụ 1.4:

# ../model/concerns/warm_up.rb
module WarmUp
  def self.included klass
    klass.extend ModuleMethods

    klass.class_eval do
      has_many :players
      validates :number_of_player, presence: true
    end
  end

  module ModuleMethods
    def run_5_round
      #..
    end
  end

  def push_ups
    #..
  end
end

# ../model/football.rb
class Football < ActiveRecord::Base
  include WarmUp
end

# ../model/kungfu.rb
class Kungfu < ActiveRecord::Base
  include WarmUp
end
$ rails c
$ puts Football.new.push_ups #OK
$ puts Football.run_5_round #OK

Module Concern

Các ví dụ trên là cách thức hoạt động của module Concern. Với việc sử dụng module này, ta có thể refactor lại phần code trong ví dụ 1.4 một cách ngắn gọn mà không làm thay đổi logic chương trình như sau:

Ví dụ 1.5:

# ../model/concerns/warm_up.rb
module WarmUp extend ActiveSupport::Concern
  included do
    has_many :players
    validates :number_of_player, presence: true

    def push_ups
      "Phew, I need a break!"
    end

    class << self
      def run_5_round
        "Running 5 km in total"
      end
    end
  end
end

# ../model/football.rb
class Football < ActiveRecord::Base
  include WarmUp
end

# ../model/kungfu.rb
class Kungfu < ActiveRecord::Base
  include WarmUp
end

Ngoài ra, module Concern còn giải quyết được vấn đề về dependency (sự phụ thuộc). Trong ví dụ sau, module Bar sẽ phụ thuộc và module Foo, và để có thể sử dụng module Bar, ta cần include đồng thời cả Foo lẫn Bar:

Ví dụ 1.6:

# test_dependency.rb

module Foo
  def self.included base
    base.class_eval do
      def self.method_of_foo
        puts "inside method of Foo"
      end
    end
  end
end

module Bar
  def self.included base
    base.method_of_foo
  end
end

class Test
  include Foo # Cần include module này do module Bar phụ thuộc vào module Foo
  include Bar # Bar mới là module thực sự cần dùng
end

Nhưng khá là vô lý khi sử dụng một module mà ta còn phải quan tâm xem nó phục thuộc vào module nào khác nữa.(kidding?) Có thể thử include trực tiếp module Foo trong module Bar như sau:

Ví dụ 1.7:

# test_dependency_fail.rb

module Foo
  def self.included base
    base.class_eval do
      def self.method_of_foo
        puts "inside method of Foo"
      end
    end
  end
end

module Bar
  include Foo
  def self.included base
    base.method_of_foo
  end
end

class Test
  include Bar
end

Tuy nhiên, thực hiện include Foo ngay trong module Bar sẽ gây lỗi, bởi trong hoàn cảnh này, “base” là Bar chứ không phải class Test, nên chương trình sẽ không thực hiện đoạn code trong block base.class_eval (khongchiudau2) . Với module Concern thì vấn đề dependency có thể hoàn toàn được giải quyết (goodjob):

Ví dụ 1.8:

# test_dependency_success.rb
require "active_support/concern"

module Foo extend ActiveSupport::Concern
  included do
    class << self
      def method_of_foo
        puts "inside method of Foo"
      end
    end
  end
end

module Bar extend ActiveSupport::Concern
  include Foo

  included do
    self.method_of_foo
  end
end

class Test
  include Bar # Class Test không quan tâm đến các module mà Bar phụ thuộc nữa
end

Tổng kết

Trên đây mình đã giới thiệu về module Concern và cách sử dụng nó trong một ứng dụng Ruby on Rails. Hi vọng bài viết sẽ giúp ích phần nào cho bạn đọc khi mới bắt đầu tìm hiểu về Ruby và Rails (yeah)(lay2).

Nguồn tham khảo

  1. http://api.rubyonrails.org/classes/ActiveSupport/Concern.html
  2. https://richonrails.com/articles/rails-4-code-concerns-in-active-record-models
  3. http://www.fakingfantastic.com/2010/09/20/concerning-yourself-with-active-support-concern/

All Rights Reserved