こんにちは、鈴木です。
今回は入力値の自動変換を題材に、メタプログラミングの話をします。
入力値の自動変換とは、例えば会員登録フォームで入力された内容を DB に保存する前(バリデーションする前)に自動変換することです。
例えば、メールアドレスは全て小文字に変換する、名前は先頭と末尾のスペースを削除する、郵便番号が全角で入力されるかもしれないので半角に変換する、といったものです。
ユーザの名前の先頭と末尾のスペースを自動的に削除する
まずはユーザの名前(User#name)が入力された時に、先頭と末尾にあるスペースを削除しようと思います。
名前の前後にスペースが入力されることなんてあるかいな。あるんです。
ユーザによる入力には予想外に揺らぎがあることが多いです。どこか別の場所からコピー&ペーストするときに不要な半角スペースが含まれてしまうこともあります。数字を入力する項目だからといって半角数字で入力されるとは限りません。メールアドレスは全て小文字で入力されることもあれば、大文字で入力されることも、はたまた大文字と小文字が混在して入力されるかもしれません。開発者が想定した形式で入力されると期待しすぎてはいけません。
話がそれましたが、入力されたユーザ名の先頭と末尾に不要なスペースがある場合は自動的に削除します。
それを実現するには、以下のように name=(value) メソッドを定義すれば達成できます。
1 2 3 4 5 6 7 8 |
class User < ActiveRecord::Base def name=(value) value = value.strip if value.kind_of?(String) write_attribute(:name, value) end end |
まだメタプログラミングはしていません。
入力された値が String であれば String#strip で先頭と末尾のスペースを削除して、その値を write_attribute で設定しています。
それでは、名前以外の属性、例えばメールアドレスについても先頭と末尾のスペースを自動的に削除したい、という場合はどうしましょうか。
先ほど定義した name=(value) メソッドをコピーして、以下のように email=(value) メソッドを書くこともできますが、コピー&ペーストしている時点で嫌な感じがします。
1 2 3 4 |
def email=(value) value = value.strip if value.kind_of?(String) write_attribute(:email, value) end |
メタプログラミング - メソッドを定義するメソッドを定義する
そこで登場するのがメタプログラミングです。
コピー&ペーストで類似したメソッドを大量生産するのではなく、先頭と末尾のスペースを削除するメソッドを定義するメソッドを定義します。
名前は attr_trimming_writer とします。
1 2 3 4 5 6 7 8 9 10 |
def self.attr_trimming_writer(name) # name=(value) は Rails によって動的に定義されるので, ここで一度アクセスします. self.new.send("#{name}=", nil) define_method("#{name}_with_trimming=") do |value| value = value.strip if value.kind_of?(String) send("#{name}_without_trimming=", value) end alias_method_chain "#{name}=", :trimming end |
そして、定義した attr_trimming_writer は以下のように使用します。
1 2 |
attr_trimming_writer :name attr_trimming_writer :email |
これでコピー&ペーストで類似メソッドをいくつも作るのではなく、attr_trimming_writer という一つのメソッドにまとめることができました。
補足ですが、上記コードでは writer_attribute で変換後の値を直接設定するのではなく、send("#{name}_without_trimming=", value) のようにしています。
これは今後の布石として、このように実装しています。
スペースの削除以外にも色々自動変換したい
ここまでは入力値の先頭と末尾のスペースを自動的に削除する、という処理を行なってきました。
しかし、スペースの削除以外にも値の自動変換を行ないたい場合もあります。
例えば、メールアドレスであれば入力値を全て小文字に変換したい、郵便番号であれば全角数値を半角数値に変換したい、という具合です。
それでは、attr_trimming_writer と同様に、小文字に変換する attr_downcase_writer、全角数値を半角数値に変換する attr_number_writer を定義してみましょう。
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 |
class User < ActiveRecord::Base def self.attr_downcase_writer(name) # name=(value) は Rails によって動的に定義されるので, ここで一度アクセスします. self.new.send("#{name}=", nil) define_method("#{name}_with_downcase=") do |value| value = value.downcase if value.kind_of?(String) send("#{name}_without_downcase=", value) end alias_method_chain "#{name}=", :downcase end def self.attr_number_writer(name) # name=(value) は Rails によって動的に定義されるので, ここで一度アクセスします. self.new.send("#{name}=", nil) define_method("#{name}_with_number=") do |value| value = value.tr('0-9', '0-9') if value.kind_of?(String) send("#{name}_without_number=", value) end alias_method_chain "#{name}=", :number end end |
attr_trimming_writer と値の変換部分以外は同じなので分かりやすいかと思います。
先ほど、「send("#{name}_without_trimming=", value) としているのは今後の布石である」と言いました。
これは、以下のように attr_xxx_writer を組み合わせて使用できるようにするためです。
1 2 |
attr_trimming_writer :email attr_downcase_writer :email |
もっとメタプログラミング - メソッドを定義するメソッドを定義するメソッドを定義する
attr_trimming_writer、attr_downcase_writer、attr_number_writer と定義してきましたが、それらのメソッドは値の変換部分以外は全て同じです。
ということは、attr_xxx_writer を定義するメソッド、というものを作ることができそうです。
実際に attr_xxx_writer を定義するメソッドを書くと、以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 |
def self.define_custom_attr_writer(conversion_name, conversion_proc) self.singleton_class.send(:define_method, "attr_#{conversion_name}_writer") do |name| # name=(value) は Rails によって動的に定義されるので, ここで一度アクセスします. self.new.send("#{name}=", nil) define_method("#{name}_with_#{conversion_name}=") do |value| value = conversion_proc.call(value) if value.kind_of?(String) send("#{name}_without_#{conversion_name}=", value) end alias_method_chain "#{name}=", conversion_name end end |
そして、define_custom_attr_writer を使用して、attr_trimming_writer、attr_downcase_writer、attr_number_writer を定義します。
1 2 3 |
define_custom_attr_writer :trimming, lambda{|value| value.strip} define_custom_attr_writer :downcase, lambda{|value| value.downcase} define_custom_attr_writer :number, lambda{|value| value.tr('0-9', '0-9')} |
そして、attr_xxx_writer を使う部分のコードは以前と同じで、次のようになります。
1 2 3 |
attr_trimming_writer :name attr_trimming_writer :email attr_downcase_writer :email |
まとめ
今回は入力値の自動変換を題材に、メタプログラミングを活用する方法についてお話ししました。
メタプログラミングのコードは、どちらかというと「ぱっと見ただけでは分かりづらい」傾向があります。
メタプログラミングは DRY (Don't Repeat Yourself) なコードにするために多大な効果を発揮しますが、デメリットも意識した上で活用したいものです。
ドキュメンテーションをしっかりすることや、メタプログラミング特有の分かりづらさをまき散らさないこと(それを使うコードに対してブラックボックスであること)が重要だと思います。
一方で、メタプログラミングによって「コンパクトなコードが書けた!」「工数が大幅に削減できた!」という時の喜びは絶大です。
用法用量を守って、節度のあるメタプログラミングライフを送りたいものです。
Enjoy Metaprogramming!!