Delegation trong Ruby

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