Delegation trong Ruby
Bài đăng này đã không được cập nhật trong 7 năm
Trong tiếng Việt, delegate được dịch là "ủy nhiệm hàm", tuy nhiên mọi người đều không sử dụng từ này mà gọi bằng tên gốc là delegate. Delegate tương tự như con trỏ hàm trong C++
Một delegate giống như một "người đại diện" hay "đại sứ". Một delegate có thể được dùng để tạo một bao đóng (encapsulation) cho bất kì phương thức nào, miễn là nó phù hợp (kiểu trả về, tham số). Là một "đại sứ", delegate có thể triệu gọi phương thức bất kì nơi nào: từ đối tượng này đến đối tượng kia, từ thread này sang thread kia
Như đã biết, Ruby là một ngôn ngữ mạnh, nó cung cấp nhiều cách, nhiều method tương tự để handle hay xử lý cùng một vấn đề. Bạn có thể thấy điều đó qua ví dụ cơ bản sau:
➜ ~ pry
[1] (pry) main: 0> a = [1, 2, 3]
=> [1, 2, 3]
[2] (pry) main: 0> a.count
=> 3
[3] (pry) main: 0> a.size
=> 3
[4] (pry) main: 0> a.length
=> 3
Với sự đa dạng các method mà Ruby cung cấp, các method có thể thay thế cho nhau trong nhiều trường hợp, tuy nhiên vẫn có sự khác biệt trong một số method có chức năng tương tự nhau. Như trong ActiveRecord, với preload hay eager_load, với joins hay includes... Bạn có luôn nhớ rằng khi nào thì sử dụng cái nào?
Việc delegation trong Ruby cũng vậy. Có một vài cách được sử dụng để delegate trong Ruby, bài viết này xin tóm tắt một vài cách thông dụng
Use case: Giả sử chúng ta vừa bước vào một tiệm bánh và muốn mua một chiếc bánh sandwich. Trong cửa hàng chỉ có một lazy employee và anh ta muốn giao công việc làm bánh cho một new intern.
Người intern của chúng ta, người trực tiếp làm bánh, có thể được mô tả như sau:
class SandwichMaker
def make_me_a_sandwich
puts 'OKAY'
end
end
I, Explicitly
Đây là cách cơ bản, rõ ràng và dễ dàng nhất để chuyển tiếp công việc của chúng ta đến một đối tượng khác. Chúng ta chỉ việc gọi một method trên một đối tượng được wrapped.
class LazyEmployee
def initialize(sandwich_maker)
@sandwich_maker = sandwich_maker
end
def make_me_a_sandwich
sandwich_maker.make_me_a_sandwich
end
private
attr_reader :sandwich_maker
end
[1] (pry) main: 0> sandwich_maker = SandwichMaker.new
=> #<SandwichMaker:0x007f8a528331a8>
[2] (pry) main: 0> lazy_employee = LazyEmployee.new(sandwich_maker)
=> #<LazyEmployee:0x007f8a52240930 @sandwich_maker=#<SandwichMaker:0x007f8a528331a8>>
[3] (pry) main: 0> lazy_employee.make_me_a_sandwich
OKAY
II, method_missing
Với phương pháp bên trên, việc gì sẽ xảy ra nếu anh intern học thêm một kĩ năng hay một method mới. Chúng ta sẽ lại phải định nghĩa một method mới cho Employee. Chúng ta có thể tránh việc đó bằng cách sử dụng method_missing:
class LazyEmployee
def initialize(sandwich_maker)
@sandwich_maker = sandwich_maker
end
def method_missing(method, *args)
if sandwich_maker.respond_to?(method)
sandwich_maker.send(method, *args)
else
super
end
end
private
attr_reader :sandwich_maker
end
[1] (pry) main: 0> sandwich_maker = SandwichMaker.new
=> #<SandwichMaker:0x007f8a521217c0>
[2] (pry) main: 0> lazy_employee = LazyEmployee.new(sandwich_maker)
=> #<LazyEmployee:0x007f8a52058780 @sandwich_maker=#<SandwichMaker:0x007f8a521217c0>>
[3] (pry) main: 0> lazy_employee.make_me_a_sandwich
OKAY
III, Forwardable
Module Forwardable cung cấp những method đặc biệt để delegation đến một đối tượng khác bằng việc sử dụng method def_delegator và def_delegators
require 'forwardable'
class LazyEmployee
extend Forwardable
def initialize(sandwich_maker)
@sandwich_maker = sandwich_maker
end
def_delegators :@sandwich_maker, :make_me_a_sandwich
end
[1] (pry) main: 0> sandwich_maker = SandwichMaker.new
=> #<SandwichMaker:0x007f8a531546d8>
[2] (pry) main: 0> lazy_employee = LazyEmployee.new(sandwich_maker)
=> #<LazyEmployee:0x007f8a52211798 @sandwich_maker=#<SandwichMaker:0x007f8a531546d8>>
[3] (pry) main: 0> lazy_employee.make_me_a_sandwich
OKAY
Method def_delegators gọi đến method make_me_a_sandwich cho đối tượng @sandwich_maker. Chi tiết hơn có thể tham khảo tại: http://ruby-doc.org/stdlib-2.0.0/libdoc/forwardable/rdoc/Forwardable.html
IV, delegate
Module#delegate cung cấp class method delegate nhằm tương tác và sử dụng các public method của một đối tượng khác như là của chính mình một cách dễ dàng.
Option
-
to: chỉ định đối tượng target
-
prefix: custom lại tên method bằng cách thêm tiền tố đứng trước tên method cũ
-
allow_nil: nếu set bằng true, sẽ ngăn việc raise ra lỗi NoMethodError.
class Greeter < ActiveRecord::Base
def hello
'hello'
end
def goodbye
'goodbye'
end
end
class Foo < ActiveRecord::Base
belongs_to :greeter
delegate :hello, to: :greeter
end
Foo.new.hello # => "hello"
Foo.new.goodbye # => NoMethodError: undefined method `goodbye' for #<Foo:0x1af30c>
Delegate nhiều method của cùng một target
class Foo < ActiveRecord::Base
belongs_to :greeter
delegate :hello, :goodbye, to: :greeter
end
Foo.new.goodbye # => "goodbye"
Những method có thể được delegate đến một biến instance, biến class hay một hằng số
class Foo
CONSTANT_ARRAY = [0,1,2,3]
@@class_array = [4,5,6,7]
def initialize
@instance_array = [8,9,10,11]
end
delegate :sum, to: :CONSTANT_ARRAY
delegate :min, to: :@@class_array
delegate :max, to: :@instance_array
end
Foo.new.sum # => 6
Foo.new.min # => 4
Foo.new.max # => 11
Hay delegate một method đến một class
class Foo
def self.hello
"world"
end
delegate :hello, to: :class
end
Foo.new.hello # => "world"
Sử dụng option prefix để custom lại tên method
Person = Struct.new(:name, :address)
class Invoice < Struct.new(:client)
delegate :name, :address, to: :client, prefix: true
end
john_doe = Person.new('John Doe', 'Vimmersvej 13')
invoice = Invoice.new(john_doe)
invoice.client_name # => "John Doe"
invoice.client_address # => "Vimmersvej 13"
class Invoice < Struct.new(:client)
delegate :name, :address, to: :client, prefix: :customer
end
invoice = Invoice.new(john_doe)
invoice.customer_name # => 'John Doe'
invoice.customer_address # => 'Vimmersvej 13'
Khi sử dụng, nếu đối tượng target là nil, sẽ raise ra lỗi NoMethodError. Để tránh việc đó, ta thêm option allow_nil: true
class User < ActiveRecord::Base
has_one :profile
delegate :age, to: :profile
end
User.new.age # raises NoMethodError: undefined method `age'
class User < ActiveRecord::Base
has_one :profile
delegate :age, to: :profile, allow_nil: true
end
User.new.age # nil
Lời kết
Trên đây là một số cách thông dụng được sử dụng để delegate trong Ruby. Thanks for read!
Nguồn:
http://apidock.com/rails/Module/delegate
https://blog.lelonek.me/how-to-delegate-methods-in-ruby-a7a71b077d99#.vce32vql8
http://ruby-doc.org/stdlib-2.0.0/libdoc/forwardable/rdoc/Forwardable.html
All rights reserved