delegationとは
delegation(委譲)はオブジェクト指向のテクニックであるが、面倒くさい点がひとつある。
それは、処理を委譲させるだけのメソッドをいちいち定義しないといけない点である。
例えば下記の様なケースを考えてみる。
class Base
def method1
puts "method1"
end
def method2
puts "method2"
end
def method3
puts "method3"
end
end
このBaseクラスのmethod1に処理を付け加え、それ以外のメソッドについてはそのままアクセスできるようにしたいとする。
継承の場合
継承を使うと下記の様になる。
class Extend < Base
def method1
print 'Hello '
super
end
end
継承を使うと、処理を付け加えたいメソッドだけ定義するだけでよく、それ以外のメソッドについては親クラスを参照するようになる。
上記のExtendクラスのインスタンスを作り、method1を実行すると'Hello method1'が出力され、method2を実行すると親クラスの処理の通り'method2'が出力される。
e = Extend.new
e.method1 # => Hello method1
e.method2 # => method2
e.method3 # => method3
継承は処理を付け加えたいメソッドだけを定義すればよい、という点においては非常に便利だが、オブジェクト同士の結びつきを強くしてしまうため、柔軟性にかけてしまう。
委譲の場合
そこで、変更したいメソッドに処理を追加し、それ以外のメソッドはBaseに処理を委譲する。
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クラスと同じ振る舞いをする。
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を実現することができる。
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メソッドで実行してみる。
b = Base.new
b.send :method1 # => method1
オブジェクトにはsendメソッドが備わっており、第一引数に呼び出すメソッド名のシンボル、それ以降の引数にメソッドに渡す引数を指定することができる。
このインターフェースはmethod_missingと同じである。
method_missingとsendを組み合わせる
先ほどのDelegationクラスを下記の様に変更してみる。
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モジュールを使う方法でいいと思う。
こういう方法もあるんだな、ぐらいにとどめておく。