こんにちは、鈴木です。
今回はメタプログラミングの練習として、同じパターンを持つコードを改良していきます。
最初のコード
何かの会員サイトを作っているとします。
まずは会員クラスが必要となるので、以下のような Member クラスを定義しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# 何かの会員サイトの会員とします. class Member < ActiveRecord::Base attr_accessible :media, :occupation # このサイトを初めて知ったメディア. MEDIA = { 1 => '新聞', 2 => 'テレビ', 3 => '週刊誌', ... } # 会員の職業. OCCUPATION = { 1 => '公務員', 2 => '会社員', 3 => '主婦', ... } # このサイトを初めて知ったメディアを文字列で返す. def media_label MEDIA[self.media] end # 会員の職業を文字列で返す. def occupation_label OCCUPATION[self.occupation] end end |
会員の情報としては、「このサイトを初めて知ったメディア」と「会員の職業」を持ちます。
データベースには数値で保存したいので、それぞれ MEDIA と OCCUPATION という定数に、数値と文字列のマッピングを保持しています。
画面などには数値ではなく文字列で表示したいので、media_label と occupation_label というメソッドを定義しました。
動作を確認すると、以下のようになります。
1 2 3 |
user = User.new(:media => 3, :occupation => 1) user.media_label # => "週刊誌" user.occupation_label # => "主婦" |
同じパターンの部分をまとめる
最初のコードを眺めていると、同じパターンの部分があることに気付きます。
以下の部分がそうです。
1 2 3 4 5 6 7 8 9 |
# このサイトを初めて知ったメディアを文字列で返す. def media_label MEDIA[self.media] end # 会員の職業を文字列で返す. def occupation_label OCCUPATION[self.occupation] end |
属性値をキーとして、定数から値を取得しています。
この部分をまとめるために、次のメソッドを作成しました。
1 2 3 4 5 |
def self.attr_label_reader(name) define_method("#{name}_label".downcase) do self.class.const_get(name.to_s.upcase)[self.send(name)] end end |
name には属性名(:media や :occupation)が渡されることを想定しています。
attr_label_reader の中では、define_method で name に「_label」を付けた名前のメソッドを定義しています。(例えば media_label や occupation_label)
定義したメソッドの中では、const_get で name を全て大文字にした定数を取得し、名前が name の属性値をキーとする値を取得します(例えば MEDIA[self.media] の値がメソッドの戻り値となります)。
まだ attr_label_reader を定義しただけなので、それを使うようにコードを修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# 何かの会員サイトの会員とします. class Member < ActiveRecord::Base attr_accessible :media, :occupation # # 指定された属性のラベルを取得するメソッドを定義する. # # ==== 詳細 # name に :media が渡された場合は以下のメソッドが定義される. # # def media_label # MEDIA[self.media] # end # # ==== 引数 # name:: 属性名. # def self.attr_label_reader(name) define_method("#{name}_label".downcase) do self.class.const_get(name.to_s.upcase)[self.send(name)] end end # このサイトを初めて知ったメディア. MEDIA = { 1 => '新聞', 2 => 'テレビ', 3 => '週刊誌', ... } # 会員の職業. OCCUPATION = { 1 => '公務員', 2 => '会社員', 3 => '主婦', ... } attr_label_reader :media attr_label_reader :occupation end |
同じ形をしていた media_label と occupation_label は以下の 2 行に置き換えることができました。
1 2 |
attr_label_reader :media attr_label_reader :occupation |
まとめ
Rails にはかゆいところに手が届く多くの機能があります。
しかし、プロジェクト固有の「かゆいところ」は必ず出てきてしまいます。
そういったときに、Railsは機能不足だと嘆くのではなく、かといって同じパターンのコードを一生懸命書いてしまうのではなく、メタプログラミングで楽をするべきだと思います。