odeの開発メモ日記

プログラマーやってます。今までの道のり ポケコンbasic→C(DirectX5 ネットやろうぜ)→Perl(ちょろっと)→Java→C#→Ruby→Android(Java)

ActiveModelにある程度機能を組み込んだベースクラス

更新履歴

  • 2011/9/28 rails3.1対応
  • 2011/6/27 ActiveModelImplの継承の継承したクラス対応。
  • 2011/6/23 リリース

概要

  • ActiveModelを使うとDBに保持しないモデルが作れます(ある程度ActiveRecord互換の機能が動く)
  • ただしそのままでは簡単に使えなそげ。機能を作るのに必要な部品を提供するものっぽい
  • なので機能をある程度組み込んだベースクラスがあると便利ではないかと作りました

組み込んだ機能

  • attributes, attributes=での設定と取得
  • dirty(どの属性が変更されたか等を取得)

下記は通常のActiveModelでも簡単に実装できるもの

  • validation
  • form_for等に渡せるように対応するため互換メソッド

ActiveModelを使うシチュエーション

  • DB使わないところ
    • 検索フォームのモデルに
    • バックエンドがDBではない場合のモデルに。APIを叩く場合等

ベースクラスのコード

class ActiveModelImpl
  include ActiveModel::Validations
  include ActiveModel::Conversion
  extend ActiveModel::Naming
  include ActiveModel::Dirty
  
  
  def initialize(attributes = {})
    @attributes = attributes_from_column_definition
    self.attributes = attributes
  end
  
  def persisted?
    false
  end
  
  def attributes=(attributes = {})
    if attributes
      attributes.each do |name, value|
        send("#{name}=", value)
      end
    end
  end
  
  def attribute_names
    @attributes.keys.sort
  end
  
  def attributes
    attrs = {}
    attribute_names.each { |name| attrs[name] = read_attribute(name) }
    attrs
  end
  
  class_attribute :column_names
  self.column_names = []
  
  def self.attr_accessor(*names)
    names.each do |attr_name|
      attr_name = attr_name.to_s
      self.column_names += [attr_name]
      generated_attribute_methods.module_eval("def #{attr_name}; read_attribute('#{attr_name}'); end", __FILE__, __LINE__)
      generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__)
    end
    define_attribute_methods names
  end
  
  def write_attribute(attr_name, value)
    attr_name = attr_name.to_s
    
    attribute_will_change!(attr_name) unless value == read_attribute(attr_name)
    
    @attributes[attr_name] = value
  end
  
  def read_attribute(attr_name)
    if respond_to? "_#{attr_name}"
      send "_#{attr_name}" if @attributes.has_key?(attr_name.to_s)
    else
      _read_attribute attr_name
    end
  end
  
  def _read_attribute(attr_name)
    attr_name = attr_name.to_s
    value = @attributes[attr_name]
    value
  end
  
  # 変更した部分の変更後のHashを返します。もともとあるchanged_attributesは変更した部分の変更前のHashを返すので。Railsのdirty機能にはなかったので実装。
  def changing_attributes
    changed.inject(HashWithIndifferentAccess.new){ |h, attr| h[attr] = __send__(attr); h }
  end
  
  # 変更履歴をクリアします。
  # モデルでsaveやcreate、updateがあるならそのなかでこのメソッドを呼ぶ。
  def clear_changed_attributes
    @previously_changed = changes
    @changed_attributes.clear      
  end
  
  private
  def attributes_from_column_definition
    self.class.column_names.inject({}) do |attributes, column_name|
      attributes[column_name] = nil
      attributes
    end
  end
  
end
  

利用コード

class User < ActiveModelImpl
  attr_accessor :name, :email

  validates_presence_of :name

  # 注意メモ
  # ・値を上書きする際は@email=とかは使わずにwrite_attributeを使う
end

user = User.new
user.attributes = {:name => "test", :email => "hoge@hogehoge.hoge"}
user.changed # ["name", "email"]
user.changes # {"name"=>[nil, "test"], "email"=>[nil, "hoge@hogehoge.hoge"]}
user.valid? # true
user.attributes # {"email"=>"hoge@hogehoge.hoge", "name"=>"test"}

実行環境

ruby (1.9.2)
rails (3.0, 3.1)