define_methods ・・ ジェネレータメソッド
sessionに出し入れするメソッドが似たようなモノがたくさん必要になったので、ジェネレータメソッドに変えてみた。
元々はこんな感じ。(何をしている部分かというと、上の「組合:サンプルマンション管理組合 → 総会:サンプル総会 → 投票人:たけ(tk)」で分かるように、最初に管理組合のリストから特定の組合を選択し、そのidをsessionに入れ、次にその組合の総会リストから特定の総会を選択し、そのidをsessionに入れ、次にその総会の、投票人リストや、議案リストや、代理人リストから特定の項目を選択する、といった処理を簡単に行うために session.xx_id=(v)メソッドを作ろう、という部分)。
class CGI::Session def soukai_id self[:soukai_id] end def soukai soukai_id && Soukai.find(soukai_id) end def soukai=(soukai) soukai_id = soukai && soukai.id return if soukai_id == self[:soukai_id] # 子レベル self.touhyounin_id = nil self.gian_id = nil # 兄弟レベル # 親レベル self.kumiai_id = soukai.kumiai_id if soukai # 自分 self[:soukai_id] = soukai_id end def soukai_id=(soukai_id) return if self[:soukai_id] == soukai_id self.soukai = soukai_id && Soukai.find(soukai_id) end def touhyounin_id self[:touhyounin_id] end def touhyounin touhyounin_id && Touhyounin.find(touhyounin_id) end def touhyounin=(touhyounin) touhyounin_id = touhyounin && touhyounin.id return if self.touhyounin_id == touhyounin_id # 子レベル # 兄弟レベル self.gian_id = nil # 親レベル self.soukai_id = touhyounin.soukai_id if touhyounin # 自分 self[:touhyounin_id] = touhyounin_id end def touhyounin_id=(touhyounin_id) return if self[:touhyounin_id] == touhyounin_id self.touhyounin = touhyounin_id && Touhyounin.find(touhyounin_id) end ・・・延々と続く・・・ end
まず
def soukai_id self[:soukai_id] end def touhyounin_id self[:touhyounin_id] end
は
class CGI::Session # # ジェネレータメソッド # def self.model_stack( model, options={} ) model = model.to_sym model_id = "#{model}_id".to_sym # define_method(model_id){ self[model_id] } end ・・・ model_stack :kumiai model_stack :soukai end
で作ることができた。
「define_method(model_id)」の「model_id」は作りたいメソッド名であり、「model_id = "#{model}_id".to_sym」で作ったシンボルが入る。「model_stack :soukai」でジェネレータを呼び出せば「"#{:soukai}_id".to_sym」→「:soukai_id」→「define_method(:soukai_id)」となって、「soukai_id」という名前のメソッドが作られる。つまり、「def soukai_id」としたのと同じことになる。
「self[model_id]」の「model_id」も「:soukai_id」となるので、「self[:soukai_id]」と同じことになる。
def kumiai kumiai_id && Kumiai.find(kumiai_id) end
は
def self.model_stack( model, options={} ) model = model.to_sym model_id = "#{model}_id".to_sym model_class = const_get(model.to_s.classify) # ←+ # define_method(model){ send(model_id) && model_class.find(send(model_id)) } end ・・・ model_stack :kumiai model_stack :soukai
「kumiai_id && Kumiai.find(kumiai_id)」は「send(model_id) && model_class.find(send(model_id))」になっている。ちと、ややこしくなったかな。
「kumiai_id」はメソッド呼び出しであり、「send(:kumiai_id)」と書くことができる。だから、「send(model_id)」でOK。
クラス名「Kumiai」は「const_get」で取り出せばよい。Kumiaiクラスというのは「Kumiai」という定数が指し示すオブジェクトなのだよ。だから「const_get」で取り出せばよい。
* ちなみに、rubyでは「kumiai_id && Kumiai.find(kumiai_id)」という論理演算は、左がnilならnil、左がnilでなければ右を返す。
* 「String#classify」は Rails が 定義しているメソッド。ruby の本体では使えない。便利といえば便利なので、ruby でも取り入れればよいのに・・。
def soukai_id=(soukai_id_v) # 引数名を変更した。 return if self[:soukai_id] == soukai_id_v self.soukai = soukai_id_v && Soukai.find(soukai_id_v) end
は
def self.model_stack( model, options={} ) model = model.to_sym model_id = "#{model}_id".to_sym model_class = const_get(model.to_s.classify) model_id_set = "#{model_id}=".to_sym # ←+ # define_method(model_id_set){|model_id_v| return if model_id_v == self[model_id] send(model_set, (model_id_v && model_class.find(model_id_v))) } end ・・・ model_stack :kumiai model_stack :soukai
で、できた。
作りたいメソッドの引数「kumiai_id_v」は define_method のブロック変数に入れる。「define_method(model_id_set){|model_id_v|」の「model_id_v」ね。
「self.soukai = ..」というのは「soukai=」メソッドの呼び出しだから「self.send(:soukai= , ..)」と同じこと。sendメソッドを使えば self は要らなくなるので、「send(:soukai= , ..)」でよい。
「:soukai=」というシンボルはあらかじめ「model_set = "#{model}=".to_sym」で作っておいたので「send(model_set , ..)」。
* ちなみに(その2)、rubyでは、属性代入メソッドは return の有無、引数に関りなく、= の右側のオブジェクトがそのまま返る。これは define_method で定義した場合でも、よきに計らってくれる。・・但し、send で実行した場合には駄目みたいだ。つまり、定義の仕方の問題ではなく、呼び出しの段階でよきに計らってくれるというわけだ。なので属性代入メソッドを send で呼び出してしまった場合には戻り値が 代入値ではない可能性があると言うことになる。しかし、それはsendで呼び出す方が悪いのだから、気にすることはない。
class Foo define_method(:foo=){|v| return 111 } end p( Foo.new.foo = 222 ) #=> 222 ・・ define_method でも OK p( Foo.new.send( :foo= , 333) ) #=> 111 ・・ send では NG
・・で、問題は、色々なバリエーションのある「def soukai=(soukai)」や「def touhyounin=(touhyounin)」。これらは次のようにした。
def self.model_stack( model, options={} ) parents = options.delete(:parents ){[]} children = options.delete(:children){[]} sisters = options.delete(:sisters ){[]} raise "invalid option (#{options.inspect})" unless options.empty? model = model.to_sym model_id = "#{model}_id".to_sym model_class = const_get(model.to_s.classify) model_set = "#{model}=".to_sym model_id_set = "#{model_id}=".to_sym ・・・ define_method(model_set){ | model_v | model_id_v = model_v && model_v.id return model_v if self.send(model_id) == model_id_v # 子レベル children.each{|child| self.send("#{child}=", nil) } yield(model_v,self) if block_given? # 兄弟レベル sisters.each{|sister| self.send("#{sister}=", nil) } # 親レベル if model_v parents.each{|parent| self.send("#{parent}_id=", model_v.send("#{parent}_id")) } end # 自分 self[model_id] = model_id_v return model_v } end ・・・ model_stack :kumiai, :children=>[:soukai] model_stack :soukai, :parents=>[:kumiai], :children=>[:touhyounin,:gian,:dairinin]
yieldの部分は次のように使う。
model_stack :gian,:parents=>[:soukai], :sisters=>[:touhyounin,:dairinin] do |gian,session| s = gian && gian.saiketu_type session[:syuukei_sanpi_narabi] = s && s.disp_sanpi_narabi session[:syuukei_saiketu] = s && s.disp_saiketu session[:syuukei_kimei] = s && s.disp_kimei session[:syuukei_bosuu] = s && s.disp_bosuu session[:syuukei_base] = s && s.disp_base end
これは元々のメソッド定義では次のようになっていた。付加的なメソッドを追加したいときに使う。
def gian=(gian) gian_id = gian && gian.id return if self.gian_id == gian_id # 子レベル s = gian && gian.saiketu_type self[:syuukei_sanpi_narabi] = s && s.disp_sanpi_narabi self[:syuukei_saiketu] = s && s.disp_saiketu self[:syuukei_kimei] = s && s.disp_kimei self[:syuukei_bosuu] = s && s.disp_bosuu self[:syuukei_base] = s && s.disp_base # 兄弟レベル self.touhyounin_id = nil self.dairinin_id = nil # 親レベル self.soukai_id = gian.soukai_id if gian # 自分 self[:gian_id] = gian_id end
で・・・・
ジェネレータメソッドにまとめる、というのは、良いことなのだろうか?
メリットとしては、(+1)コードが短くなること、(+2)間違いを見つけやすくなること(実際、まとめてみたら一つ間違いを見つけてしまった)。(+3)変更したときに、一度に全部変更できること。
デメリットとしては、(−1)「model_stack :kumiai」と書けば「session.kumiai」 「session.kumiai_id」 「session.kumiai=(v)」 「session.kumiai_id=(v)」という四つのメソッドが定義されるよ、ということは、「model_stack :kumiai」という記述から自明のこととして理解するのは困難だ、ということ。(−2)変更するのが難しくなるかもしれない。(−3)とくに、共通部分と特異部分との切り分けの変更が必要になったときに、改造は困難になる。