Leon's Blogging

Coding blogging for hackers.

Ruby - Forwardable 轉發 & Delegate 委派

| Comments

在 OO 的世界裡,經常會去用到不同 class 的 method,因此透過這個方法,可以將不同 class 的 method 轉發 過來!

Forwardable

顧名思義,將訊息 轉發 給別的物件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Account = Struct.new(:first_name, :last_name)

class User
  attr_reader :account

  def initialize(account)
    @account = account
  end

  def first_name
    account.first_name
  end

  def last_name
    account.last_name
  end

  def full_name
    "#{first_name} #{last_name}"
  end
end

user = User.new(Account.new('foo', 'bar'))

puts user.first_name
puts user.last_name
puts user.full_name
#=>foo
#=>bar
#=>foo bar

上述有許多重複的 account,此時就可以使用 Forwardable 來簡化: 將 first_name、last_name 轉發 給 account。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
require 'forwardable'

Account = Struct.new(:first_name, :last_name)

class User
  attr_reader :account
  extend Forwardable
  def_delegators :account, :first_name, :last_name

  #若 User 不想對外開放,attr_reader :account,可以改成實例變數,如以下
  #extend Forwardable
  #def_delegators :@account, :first_name, :last_name

  def initialize(account)
    @account = account
  end

  def full_name
    "#{first_name} #{last_name}"
  end
end

user = User.new(Account.new('foo', 'bar'))

puts user.first_name
puts user.last_name
puts user.full_name
#=>foo
#=>bar
#=>foo bar

def_delegator(accessor, method, ali = method)

一次只能 ‘轉發’ 一個方法,第三個參數是(可選的)別名。

class MyQueue
  extend Forwardable
  attr_reader :queue
  def initialize
    @queue = []
  end

  def_delegator :@queue, :push, :mypush
end

# q = MyQueue.new
# q.mypush 42
# q.queue    #=> [42]
# q.push 23  #=> NoMethodError

def_delegators(accessor, *methods)

一次可以 ‘轉發’ 多個方法。

1
def_delegators :@account, :first_name, :last_name

delegate [method, method, …] => accessor

接受Hash

1
delegate [:first_name, :last_name] => :account

看是如何 work

1
2
3
4
5
6
7
8
9
10
11
12
13
module Forwardable
  def delegate(hash)
    hash.each{ |methods, accessor|
      methods.each{ |method|
        instance_eval %{
          def #{method}(*args, &block)
            #{accessor}.__send__(:#{method}, *args, &block)
          end
        }
      }
    }
  end
end

example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
require 'forwardable'
class Bicycle
  attr_reader :size, :parts

  def initialize(args={})
    @size  = args[:size]
    @parts = args[:parts]
  end

  def spares
    parts.spares
  end
end

class Parts
  extend Forwardable
  #@parts 是一個 Array,因此將 size 和 each 委派給 Parts 實例物件
  def_delegators :@parts, :size, :each
  #為了要讓實例可以直接用 select,select 又會用到 each 所以上面要委派
  include Enumerable

  def initialize(parts)
    @parts = parts
  end

  def spares
       #所以這裡才可以直接用 select,不用 @parts.select
    select{ |part| part.needs_spare}
  end
end

class Part
  attr_reader :name, :description, :needs_spare

  def initialize(args)
    @name        = args[:name]
    @description = args[:description]
    @needs_spare = args.fetch(:needs_spare, true)
  end
end

chain     = Part.new(name: 'chain',     dsescription: '10-speed')
tire_size = Part.new(name: 'tire_size', description: '23')
tap_color = Part.new(name: 'tap_color', description: 'red')


road_bike = Bicycle.new(
              size:'L',
              parts: Parts.new([chain, tire_size, tap_color])
              )

puts road_bike.parts.size
puts '*'*10
puts road_bike.spares.size
puts '*'*10

Other

  • instance_delegate alias delegate
  • def_instance_delegator alias def_delegator
  • def_instance_delegators alias def_delegators

  • rails-delegate 之前就有寫到,可以參考!

官方文件:

參考文件:

Comments