読者です 読者をやめる 読者になる 読者になる

雑草SEの備忘録

[旧:東大卒SEの備忘録]東大を卒業し、SEとして働くことになった。備忘録的に作業を綴る。

Ruby on Rails初心者が一歩進むために~1~

Ruby Rails

タイトルの通り。Ruby on Railsを勉強していてこの使い方知ってるとだいぶ幅が広がるなというのがあったので、メモ。

今回は、define_methodとdefine_singleton_methodです。キーワードはメソッドの動的生成。

1.define_method

define_method (Module) - Rubyリファレンス

メソッドを定義することができる。
同じ処理を何度もしたい!けどいちいち一つずつ定義するのは面倒だ!なんてときに使います。繰り返し似たようなメソッドを作るときには売ってつけです。
たとえば、次のようなCurrencyモデルがあったとします。

Currencyモデル

id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
code varchar(255)
appelation varchar(255)

currenciesテーブルのデータ

id code appellation
1 JPY
2 USD ドル
3 TWD 台湾ドル

そして、例えば、一つデータをとってくる。

[2] pry(main)> currency = Currency.first
  Currency Load (0.4ms)  SELECT  "currencies".* FROM "currencies"   ORDER BY "currencies"."id" ASC LIMIT 1
=> #<Currency id: 1, code: "JPY", appelation: "円">

ここで、とってきたデータがJPYかどうかを調べたいというとき。Currencyモデルにjpy?というメソッドを作る。

class Currency < ActiveRecord::Base
  def jpy?
    code == 'JPY'
  end
end

こんな感じでjpy?というメソッドを追加します。
さて、同様にusd?とかtwd?とかいうメソッドも追加したい。だけれども、今後通貨が増えてくるし、いちいちメソッドを定義するのは面倒だ!という時に、一気にメソッドを定義してくれるのがdefine_method。
例えば、CODESという通貨コードをもった配列を用意して、くるくるまわす。

class Currency < ActiveRecord::Base
  CODES = ['JPY', 'USD', 'TWD']

  CODES.each do |currency_code|
    define_method("#{currency_code.downcase}?".to_sym) { code == currency_code }
  end

  # 上のブロック内のdefine_methodによって以下が定義されたのと同じ。
  #def jpy?
  #  code == 'JPY'
  #end
  #def usd?
  #  code == 'USD'
  #end
  #def twd?
  #  code == 'TWD'
  #end
end

結果がこちら。きちんとメソッドが定義されていて、期待通りの動きになっていることが分かる。

[1] pry(main)> currency = Currency.find(2)
  Currency Load (0.8ms)  SELECT  "currencies".* FROM "currencies"  WHERE "currencies"."id" = ? LIMIT 1  [["id", 2]]
=> #<Currency id: 2, code: "USD", appelation: "ドル">
[2] pry(main)> currency.jpy?
=> false
[3] pry(main)> currency.usd?
=> true
[4] pry(main)> currency.twd?
=> false
2.define_singleton_method

define_singleton_method (Object) - Rubyリファレンス

こちらも基本的には、define_methodと同じです。ただし、特異メソッドを作ってくれます。Railsだと、例えば、モデルにクラスメソッドを動的に作成したいときに威力を発揮します。
今回の例ですと

Currency.twd

とやると、台湾ドルのデータをとってこれるようにしたいという場合を考えます。
単純にクラスメソッドを定義してやればいいです。

class Currency < ActiveRecord::Base
  def self.twd
    where(code: 'TWD').first
  end
end

ただ、これも同様に日本円やUSドルも同様に作りたいですよね。define_methodと同じようにすればいいです。define_singleton_methodをつかうと、クラスメソッドをはやすことができます。

class Currency < ActiveRecord::Base
  CODES = ['JPY', 'USD', 'TWD']

  CODES.each do |currency_code|
    define_singleton_method("#{currency_code.downcase}".to_sym) do
      where(code: currency_code).first
    end
  end

  # 上のブロック内のdefine_singleton_methodによって以下が定義されたのと同じ。
  #def self.jpy
  #  where(code: 'JPY').first
  #end
  #def self.usd
  #  where(code: 'USD').first
  #end
  #def self.twd
  #  where(code: 'TWD').first
  #end
end

rails consoleで動作を確認してみます。

[1] pry(main)> Currency.jpy
  Currency Load (0.2ms)  SELECT  "currencies".* FROM "currencies"  WHERE "currencies"."code" = 'JPY'  ORDER BY "currencies"."id" ASC LIMIT 1
=> #<Currency id: 1, code: "JPY", appelation: "円">
[2] pry(main)> Currency.usd
  Currency Load (0.5ms)  SELECT  "currencies".* FROM "currencies"  WHERE "currencies"."code" = 'USD'  ORDER BY "currencies"."id" ASC LIMIT 1
=> #<Currency id: 2, code: "USD", appelation: "ドル">
[3] pry(main)> Currency.twd
  Currency Load (0.4ms)  SELECT  "currencies".* FROM "currencies"  WHERE "currencies"."code" = 'TWD'  ORDER BY "currencies"."id" ASC LIMIT 1
=> #<Currency id: 3, code: "TWD", appelation: "台湾ドル">