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)とくに、共通部分と特異部分との切り分けの変更が必要になったときに、改造は困難になる。