Forwardable 모듈

Forwardable 모듈 (Forwardable)

Forwardable 모듈은 def_delegatordef_delegators 메서드를 이용해서, 지정된 메서드들을 특정 객체로 위임(delegation)해 주는 기능을 제공해요.

예를 들어 @records 배열을 담고 있는 RecordCollection 클래스가 있다고 해볼게요. 배열의 []를 그냥 호출하는 조회 메서드 record_number()를 이렇게 만들 수 있어요.

require 'forwardable'

class RecordCollection
  attr_accessor :records
  extend Forwardable
  def_delegator :@records, :[], :record_number
end

조회 메서드는 이렇게 사용해요.

r = RecordCollection.new
r.records = [4,5,6]
r.record_number(0)  # => 4

나아가 size, << 같은 메서드도 제공하고 싶다면 클래스를 다시 열어 위임을 추가할 수 있어요.

class RecordCollection # re-open RecordCollection class
  def_delegators :@records, :size, :<<
end

r = RecordCollection.new
r.records = [1, 2, 3, 4]
r.size               # => 4
r << 1               # => [1, 2, 3, 4, 1]
r.map { |x| x * 2 }  # => [2, 4, 6, 8]

일반 객체에 Forwardable을 extend해서 위임을 준비할 수도 있어요.

my_hash = Hash.new
my_hash.extend Forwardable              # prepare object for delegation
my_hash.def_delegator "STDOUT", "puts"  # add delegation for STDOUT.puts()
my_hash.puts "Howdy!"

출처: Ruby 3.3 API

본문

상속 대신 위임 (Delegate Instead of Inheritance)

슈퍼클래스의 모든 메서드를 상속받고 싶지 않다면, Forwardable을 상속의 대안으로 쓸 수 있어요. 예를 들어 배열의 인스턴스 메서드 일부를 새 Queue 클래스에 추가해 볼게요.

class Queue
  extend Forwardable

  def initialize
    @q = [ ]    # prepare delegate object
  end

  # setup preferred interface, enq() and deq()...
  def_delegator :@q, :push, :enq
  def_delegator :@q, :shift, :deq

  # support some general Array methods that fit Queues well
  def_delegators :@q, :clear, :first, :push, :shift, :size
end

q = Thread::Queue.new
q.enq 1, 2, 3, 4, 5
q.push 6

q.shift    # => 1
while q.size > 0
  puts q.deq
end

q.enq "Ruby", "Perl", "Python"
puts q.first
q.clear
puts q.first

이 코드는 이렇게 출력해요.

2
3
4
5
6
Ruby
nil

참고 (Notes)

RDoc은 위임된 메서드를 감지하지 못하는 점을 유의하세요. 또한 forwardable.rbdef_delegator/def_delegators를 통한 단일 메서드 위임을 제공하고, DelegateClass를 통한 전체 클래스 위임은 delegate.rb를 사용해요.

상수와 속성 (Constants and Attributes)

  • FORWARDABLE_VERSION, VERSIONforwardable.rb의 버전
  • debug[RW] 속성 (무시됨)

#def_instance_delegator (Public Instance Method)

def_instance_delegator(accessor, method, ali = method) — 메서드를 위임 인스턴스 메서드로 정의해요. 선택적으로 별칭 이름 ali를 줄 수 있어요. ali에 대한 호출은 accessor.method로 위임돼요. accessor는 메서드 이름, 인스턴스 변수 이름, 또는 상수 이름이 될 수 있어요(상수는 전체 경로). 정의된 메서드 이름을 돌려줘요. def_delegator의 별칭이에요.

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

  def_delegator :@queue, :push, :mypush
  def_delegator 'MyQueue::CONST', :to_i
end

q = MyQueue.new
q.mypush 42
q.queue    #=> [42]
q.push 23  #=> NoMethodError
q.to_i     #=> 1
# File lib/forwardable.rb, line 188
def def_instance_delegator(accessor, method, ali = method)
  gen = Forwardable._delegator_method(self, accessor, method, ali)

  # If it's not a class or module, it's an instance
  mod = Module === self ? self : singleton_class
  ret = mod.module_eval(&gen)
  mod.__send__(:ruby2_keywords, ali) if RUBY_VERSION >= '2.7'
  ret
end

#def_instance_delegators (Public Instance Method)

def_instance_delegators(accessor, *methods) — 별칭 없이 여러 위임 메서드를 한 번에 정의하는 단축 메서드예요. 아래 두 코드는 같은 효과를 가져요(def_delegators의 별칭).

def_delegators :@records, :size, :<<
# File lib/forwardable.rb, line 156
def def_instance_delegators(accessor, *methods)
  methods.each do |method|
    next if /\A__(?:send|id)__\z/ =~ method
    def_instance_delegator(accessor, method)
  end
end

#instance_delegate / #delegate

delegate method → accessor, delegate [method, method, ...] → accessorinstance_delegate 메서드의 별칭이에요. forwardabledef_delegator 대신 사용할 수 있어요.