LoginSignup
22
22

More than 5 years have passed since last update.

ActiveModel::AttributeMethods で重複をなくす

Posted at

AttributeMethods のできること

指定した属性に対して、まるっと同じ機能を実装する事が出来ます。
例えば、こんな感じ

class Person
  include ActiveModel::AttributeMethods

  attr_accessor :name, :age, :address
  attribute_method_prefix 'clear_'
  define_attribute_methods :name, :age, :address

  def initialize(name:, age:, address:)
    @name = name
    @age = age
    @address = address
  end

  private

  def clear_attribute(attr)
    send("#{attr}=", nil)
  end
end

person = Person.new
person.name = 'Bob'
person.name         #=> "Bob"
person.clear_name
person.name         #=> nil
person.age = 20
person.clear_age
person.age          #=> nil

複数の属性に共通の処理をさせたいとき、重複なく書けていいですね。
そんな、AttributeMethods にあるメソッドを紹介します。

attribute_method_prefix(*prefixes)

define_attribute_methods で宣言した、各属性に対して、prefix を付加したメソッドを定義出来ます。
メソッドの定義は #{prefix}_attribute で定義して、属性名を第一引数に取ります。

class Person
  include ActiveModel::AttributeMethods

  attr_accessor :name
  attribute_method_prefix 'titleize_'
  define_attribute_methods :name

  private

  def titleize_attribute(attr)
    original = send(attr)
    titleize_str = original.split(' ').map(&:capitalize).join(' ')
    send("#{attr}=", titleize_str)
  end
end

person.name = 'tanaka taro'
person.titleize_name
person.name          #=> "Tanaka Taro"

attribute_method_suffix(*suffixes)

名前から分かるように上の suffix 版です。attribute_#{suffix} を定義することが出来ます。

class Person
  include ActiveModel::AttributeMethods

  attr_accessor :name
  attribute_method_suffix '_short?'
  define_attribute_methods :name

  private

  def attribute_short?(attr)
    send(attr).length < 5
  end
end

person.name = 'Bob'
person.name_short? # => true

attribute_method_affix(*affixes)

そして、prefix と suffix の両方をつけたいときです!

class Person
  include ActiveModel::AttributeMethods

  attr_accessor :name
  attribute_method_affix prefix: 'reset_', suffix: '_to_default!'
  define_attribute_methods :name

  private

  def reset_attribute_to_default!(attr)
    send("#{attr}=", 'Default Name')
  end
end

person.name = 'Bob'
person.reset_name_to_default!
person.name                   #=> "Default Name"

中を見てみる

上の三つで、まるっと便利にメソッドを定義出来ますが、一体こいつらがどんな事をやっているのかを見てみると、やはりお得意の method_missing を使ってます。

def attribute_method_prefix(*prefixes)
  self.attribute_method_matchers += prefixes.map! { |prefix| AttributeMethodMatcher.new prefix: prefix }
  undefine_attribute_methods
end

定義したいメソッド名を宣言する attribute_method_prefix です。中を見ると AttributeMethodMatcher のインスタンスを繰り返し作ってますね。

class AttributeMethodMatcher
  ...

  def initialize(options = {})
    @prefix, @suffix = options.fetch(:prefix, ''), options.fetch(:suffix, '')
    @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
    @method_missing_target = "#{@prefix}attribute#{@suffix}"
    @method_name = "#{prefix}%s#{suffix}"
  end

  ...
end

AttributeMethodMatcher が作られると @regex に正規表現を挿入してる事がわかります。この正規表現にマッチするものを method_missing でキャッチして、ゴリゴリとメソッドを呼び出しているんですね。

おわり

AttributeMethods で、いろいろと DRY に実装できそうですね。んーそれにしても method_missing は便利すぎて恐ろしいなあ。

22
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
22