Edited at

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

More than 3 years have passed since last update.


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モジュールを使う方法でいいと思う。

こういう方法もあるんだな、ぐらいにとどめておく。