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 # => []