Ruby

メソッドの委譲機能

More than 5 years have passed since last update.

ActiveRecordのモデルを作っていると親子関係のクラスがたくさん出来ますが、子のクラスオブジェクトから親のクラスに実装されたメソッドを呼ぶことがあります。
そんなときに、コードをより直感的かつスマートに記述することができる便利な機能を教えてもらいました。

qiita.rb
# parent.rb
class Parent < ActiveRecord::Base
  has_many :children

  def has_parent?
    ...
  end
end

# child.rb
class Child < ActiveRecord::Base
  belongs_to :parent
end

> child = Child.find(1)
> child.parent.has_parent? # 冗長な感じがする

# child.rb をこう書くと
require 'forwardable'
class Child < ActiveRecord::Base
  extend Forwardable

  belongs_to :parent
  def_delegator :parent, :has_parent?, :has_grandparent?
end

# こう書ける!
> child.has_grandparent?

委譲機能について

オブジェクトの機能を再利用するには、クラス継承やモジュールのMix-inを利用する方法があります。しかし、これらの方法は元になるクラスやモジュールの実装をそのまま取り込んでしまいます。
委譲では、再利用したい機能を取り込むのではなく、その機能を持つオブジェクトに処理を依頼することで、機能の再利用を実現させます。

forwardable

forwardableには2つのモジュールが定義されており、それぞれ

  • Forwardable クラスに対してメソッドの委譲機能を定義する
  • SingleForwardable オブジェクトに対してメソッドの委譲機能を定義する

となっています。
Forwardableを使ってArrayクラスの機能を再利用するコードを書いてみる。

qiita.rb
require 'forwardable'
class MyClass
  extend Forwardable
  # 実装の動きが追いやすいように、オブジェクトからアクセス可能にしておく
  attr_accessor :ary

  def initialize
    @ary = Array.new
  end

  # 配列の要素を追加する機能の委譲をする
  def_delegator :ary, :push
end

pushメソッドが思惑通りに動いているか、試してみる。

qiita.rb
obj = MyClass.new
obj.ary # => []

obj.push 'one' # => ["one"]
obj.push 'two', 'three' # => ["one", "two", "three"]

さらに、このクラスオブジェクトにSingleForwardableを使って、他の機能を委譲してみる。

qiita.rb
obj.extend SingleForwardable
# 配列の要素を取り出す機能の委譲をする
obj.def_delegator :ary, :shift

obj.shift # => "one"
obj.ary # => ["two", "three"]

ActiveRecordのケースのように、機能の一部を再利用したいケースでは継承やMix-inより委譲するほうが向いていて、扱いも簡単で便利でした。

def_delegatorとdef_delegatorsの使い方

qiita.rb
  # 第3引数はエイリアスになる
  obj.def_delegator :ary, :push, :add
  obj.add "four" # => ["two", "three", "four"]

  # 複数の機能をリストで委譲する
  obj.def_delegators :ary, :pop, :clear
  obj.pop # => "four"
  obj.ary # => ["two", "three"]
  obj.clear # => []