Rails ActiveSupport Concerns
Buffet / 09 May 2019Concern là gì
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.
Chức năng của Concern
Chức năng của Concern = Module + extends methods in included callback
Module cho phép gom methods lại thành nhóm và sau đó các methods này được sử dụng bằng cách include module chứa chúng vào trong module/class khác.
# ../model/concerns/engine.rb
module Engine
def start_engine
"Jarvis, start the engine!"
end
end
# ../model/tank.rb
class Tank < ActiveRecord::Base
include Engine
def run
"I'm running."
end
end
# ../model/iron_man.rb
class IronMan < ActiveRecord::Base
include Engine
def snap_fingers
"I am Iron Man!"
end
end
Sử dụng
$ rails c
$ puts Tank.new.start_engine
Jarvis, start the engine!
$ puts IronMan.new.start_engine
Jarvis, start the engine!
Hạn chế của việc class include module đó là class đó chỉ có thể truy cập tới instance methods của module mà không thể truy cập tới các class methods
Một cách giải quyết là ta nhóm các class methods trong một module khác và extend nó trong included callback, đồng thời trong callback này, có thể viết các method về validates, quan hệ, scope.. để các model có thể tái sử dụng
# ../model/concerns/engine.rb
module Engine
def self.included klass
klass.extend ModuleMethods
klass.class_eval do
has_one :piston
validates :engine_type, presence: true
end
end
module ModuleMethods
def missile_launch
"Fire!!"
end
end
def start_engine
"Jarvis, start the engine!"
end
end
# ../model/tank.rb
class Tank < ActiveRecord::Base
include Engine
end
# ../model/iron_man.rb
class IronMan < ActiveRecord::Base
include Engine
end
$ rails c
$ puts IronMan.new.start_engine #OK
$ puts IronMan.missile_launch #OK
Refactor lại 2 ví dụ trên sử dụng Concern
# ../model/concerns/engine.rb
module Engine extend ActiveSupport::Concern
included do
has_one :piston
validates :engine_type, presence: true
def start_engine
"Jarvis, start the engine!"
end
class << self
def missile_launch
"Fire!!"
end
end
end
end
# ../model/tank.rb
class Tank < ActiveRecord::Base
include Engine
end
# ../model/iron_man.rb
class IronMan < ActiveRecord::Base
include Engine
end
Giải quyết vấn đề về dependency
Trong ví dụ sau, module Bar sẽ phụ thuộc và module Foo, để sử dụng module Bar, cần include đồng thời cả Foo lẫn Bar trong class Test:
# 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
Khá là vô lý khi sử dụng một module mà còn phải quan tâm xem nó phục thuộc vào module nào khác nữa. Vì vậy có thể thử include trực tiếp module Foo trong module Bar
# 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
==> gây lỗi undefined method 'method_of_foo' for Test:Class
, 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
Giải quyết vấn đề này bằng việc dùng Concern
# 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