Rubyでdelegation(委譲)を簡単にする2つの方法

  • 162
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

delegationとは

delegation(委譲)はオブジェクト指向のテクニックであるが、面倒くさい点がひとつある。

それは、処理を委譲させるだけのメソッドをいちいち定義しないといけない点である。
例えば下記の様なケースを考えてみる。

Base.rb
class Base
  def method1
    puts "method1"
  end

  def method2
    puts "method2"
  end

  def method3
    puts "method3"
  end
end

このBaseクラスのmethod1に処理を付け加え、それ以外のメソッドについてはそのままアクセスできるようにしたいとする。

継承の場合

継承を使うと下記の様になる。

Extend.rb
class Extend < Base
  def method1
    print 'Hello '
    super
  end
end

継承を使うと、処理を付け加えたいメソッドだけ定義するだけでよく、それ以外のメソッドについては親クラスを参照するようになる。

上記のExtendクラスのインスタンスを作り、method1を実行すると'Hello method1'が出力され、method2を実行すると親クラスの処理の通り'method2'が出力される。

extend_sample.rb
e = Extend.new
e.method1 # => Hello method1
e.method2 # => method2
e.method3 # => method3

継承は処理を付け加えたいメソッドだけを定義すればよい、という点においては非常に便利だが、オブジェクト同士の結びつきを強くしてしまうため、柔軟性にかけてしまう。

委譲の場合

そこで、変更したいメソッドに処理を追加し、それ以外のメソッドはBaseに処理を委譲する。

Delegation.rb
class Delegation
  def initialize(base)
    @base = base
  end

  def method1
    print 'Hello '
    @base.method1
  end

  def method2
    @base.method2
  end

  def method3
    @base.method3
  end
end

上記の例は、Extendクラスと同じ振る舞いをする。

delegation_sample.rb
d = Delegation.new(Base.new)
d.method1 # => Hello method1
d.method2 # => method2
d.method3 # => method3

委譲のメリット・デメリット

委譲のメリットは継承に比べて柔軟なところである。
例えば、Delegationクラスはmethod3を使用する必要がないのであれば、定義しなければいいだけである。
こういったスコープのコントロールもやりやすくなる。

また、Delegationクラスはmethod2とmethod3の中身を知る必要はない。
呼び出しが行われたら、自分の担当ではないので、Baseクラスに横流しするだけである。
こうすることで、処理の分離をはっきりさせることができる。

逆にデメリットは、冒頭でも記述したが委譲させるだけのメソッドを定義しなければいけない点である。
今回の例ではメソッドが3つしかないため、そこまで苦労はしなかったが、これが100、200となっていった時には、心が折れてしまう。

Delegationを簡単に実装する

Rubyにはこの委譲テクニックを簡単に実装する2つ方法がある。

1. Forwardableモジュールを使用する

RubyにはデフォルトでForwardableモジュールが備わっている。
このモジュールを使うと下記の様にDelegationを実現することができる。

forwardable_sample.rb
require 'forwardable'

class Delegation
  extend Forwardable

  def_delegators :@base, :method2, :method3

  def initialize(base)
    @base = base
  end

  def method1
    print 'Hello '
    @base.method1
  end
end

Forwardableモジュールは、def_delegatorsメソッドを持っており、第一引数には委譲先のオブジェクト、それ以降の引数で委譲したいメソッド名を指定する。
def_delegatorsメソッドは指定されたすべてのメソッドをクラスに追加するため、先の例で挙げたものと同じ挙動をするようになる。

2. method_missingメソッドの活用

method_missingメソッドは、存在しないメソッドが呼び出された際に実行されるメソッドで、第一引数には呼びだそうとしたメソッド名のシンボル、それ以降の引数では呼び出された時の引数が渡される。

このmethod_missingとRubyに備えられているsendメソッドを組み合わせると委譲が簡単に実現できる。

sendメソッドとは

先のBaseクラスのメソッドをsendメソッドで実行してみる。

send_sample.rb
b = Base.new
b.send :method1 # => method1

オブジェクトにはsendメソッドが備わっており、第一引数に呼び出すメソッド名のシンボル、それ以降の引数にメソッドに渡す引数を指定することができる。

このインターフェースはmethod_missingと同じである。

method_missingとsendを組み合わせる

先ほどのDelegationクラスを下記の様に変更してみる。

Delegation.rb
class Delegation
  def initialize(base)
    @base = base
  end

  def method1
    print 'Hello '
    @base.method1
  end

  def method_missing(name, *args)
    @base.send name, *args
  end
end

method1は定義されているが、method2とmethod3は定義されていない。
そのため、method2とmethod3を呼び出しをするとmethod_missingが実行される。
後は、このmethod_missingに渡ってきた内容を、そのままBaseクラスにsendメソッドを使って受け流せば、無事処理は実行される。

まとめ

method_missingを使うパターンは簡単ではあるが、実行速度が遅くなってしまう、コードがわかりにくくなってしまう、などという代償がある。
そのため、基本的にはForwardableモジュールを使う方法でいいと思う。
こういう方法もあるんだな、ぐらいにとどめておく。