オブジェクト指向プログラミングでは、「委譲」と呼ばれる手法があります。今回は、Rubyで委譲を行う'forwardable'について触れてみます。
利用例
たとえば、親クラス(Parentとします)があるハッシュをメンバに持っていて、しかも
-
Parentからは、このハッシュを変更できる。 - ハッシュ自体は外部で変更されたくはないが、読み取り用としてはハッシュとして外部でも使って欲しい
- 外部から使うたびにコピーするにはハッシュが大きすぎる
というような状況にあるとします。このようなときに、「委譲」を使うことができます。
forwardableモジュール
Rubyに標準添付されているforwardableというライブラリがあって、「指定されたメソッドを、特定のオブジェクトに実行させる」ことができます。クラスにextend Forwardableとすることで、attr_accessorのようなクラスメソッドとして、def_delegatorsが使えるようになります。
class Foo
extend Forwardable
def_delegators :@item, :method_1, :method_2 #...
end
このようにすると、Fooクラスのオブジェクトにmethod_1やmethod_2のようなメソッドができて、それはインスタンス変数の@itemへの呼び出しになります。
includesとの併用
このような形で委譲をかける場合に、EnumerableやComparableがあるクラスですべてをやろうとすると、手間になってしまいます。ただ、Enumerableはeachだけあれば、Comparableは<=>だけあれば動くので、そのキーになるメソッドだけ委譲して、残りは直接includeしてしまう形で問題ありません。
class Bar
extend Forwardable
include Comparable
include Enumerable
#includeしたのを動かすのに必要十分なメソッドだけ借りてくる
def_delegators :@item, :each, :<=>
end
最初の例の実装
では、最初のユースケースについてフル実装します。
require 'forwardable'
class Parent
class ReadOnlyHash
extend Forwardable
include Enumerable
# 破壊的変更をしないメソッドだけ読み込む
def_delegators :@hash, :each, :[], :fetch, :empty?, :clone, :dup
def_delegators :@hash, :each_pair, :each_key, :each_value, :keys, :values
def_delegators :@hash, :has_key?, :key?, :include?
# 多いので以下省略
def initialize(hash)
@hash = hash
end
end
#内部的にいろいろ操作する
@hash = { }
#読み取りしかできないものを返す
def hash
ReadOnlyHash.new @hash
end
end
その他
これはHashをfreezeした場合にも通じますが、ハッシュの中身が文字列やコンテナなどの場合、要素への破壊的変更は行えてしまいますので、その点には注意が必要です。