What is it, naokirin?

ActiveModel::Model で DBに依存しないモデルを作ろう

もう使い古されたネタという感じですが、Railsを使わずとも ActiveSupport だけを利用してRubyのコードを書くことは結構多いのではないでしょうか。

blank? , present? であったり、 pluralizetry1.year.ago のような呼び出しなど、ヘルパーメソッドはRubyの標準ライブラリだけではなかなかスッキリとかけないところに手が届き、非常に重宝します。また ActiveSupport::Concern は Mix-in をする際に非常に便利です。

が、今回はRailsActiveRecord インスタンスのように扱えるようになる ActiveModel::Model に注目してみることにしました。

ちなみに、ActiveSupport の Core Extensions については、以下を参考にするのが良いかと思います。

Active Support コア拡張機能 - Rails ガイド

ちなみにですが、バージョンはRails の v5.2.2 を元にしています。

ActiveModel::Model モジュール

ActiveModel::Model はコードを見るとわかりますが、以下のモジュールの組み合わせになっています。

extend ActiveSupport::Concern
include ActiveModel::AttributeAssignment
include ActiveModel::Validations
include ActiveModel::Conversion

included do
  extend ActiveModel::Naming
  extend ActiveModel::Translation
end

参考: https://github.com/rails/rails/blob/v5.2.2/activemodel/lib/active_model/model.rb#L60-L68

ActiveSupport::ConcernActiveModel::Model モジュール自体の実装に使われていますが、それ以外は include ActiveModel::Model したクラスやモジュールで利用することになります。また、明示されていませんが、 ActiveModel::Callbacks も include されます。

それぞれのモジュールについてざっくりと説明すると以下のようになります。

  • ActiveModel::AttributeAssignment
    • attr_accessor で定義されたアクセサに対して、一括で値を設定することができる機能を提供する
  • ActiveModel::Validations
    • モデルの検証用の機能を提供する
  • ActiveModel::Conversion
    • Rails のモデルの変換メソッド( to_model , to_key , to_param , to_partial_path )を利用できるようにする
  • ActiveModel::Naming
    • モデル名に対するヘルパクラスメソッドを提供する
  • ActiveModel::Translation
    • オブジェクトとi18nの間の統合機能を提供する
  • ActiveSupport::Callbacks
    • ActiveRecord 形式のコールバックの機能を提供する

ActiveRecordでないクラスをモデルのように扱う

では、サンプルコードを書いてみたので見てみましょう。 今回は ActiveModel::Attributes も利用してみました。

require 'active_model'

class CustomModel
  include ActiveModel::Model
  include ActiveModel::Attributes

  # ActiveModel::Attributes で属性を定義する
  attribute :id, :integer, default: 0
  attribute :name, :string, default: ''

  # ActiveModel::Validations の機能で妥当性検証用の設定を追加をする
  validates :id, presence: true, numericality: { only_integer: true, greater_than: 0 }
  validates :name, presence: true

  # ActiveModel::Callbacks の機能で save メソッドの前に妥当性検証をする
  define_model_callbacks :save, only: :before
  before_save { throw(:abort) if invalid? }

  def save
    # ActiveModel::Callbacks のコールバック呼び出しができるようにする
    run_callbacks :save do
      puts 'Saved!'
    end
  end
end

上記のように定義をすると、以下のように利用することができます。

obj = CustomModel.new

# ActiveModel::AttributeAssignment による値の設定
obj.attributes = {id: 1, name: 'Bob' } 

# 妥当性チェック & コールバック呼び出し
# 条件を満たしているので `puts 'Saved!'` が呼ばれる
obj.save

# ActiveModel::Attributes  により、適切なキャストが行われる
obj.id = '0'

# 妥当性チェック & コールバック呼び出し
# 条件を満たしていないので `puts 'Saved!'` が呼ばれない
obj.save

多少、コールバックのためにひと手間実装が必要だったりしますが、Railsでの実装でもDBに直接紐付かないモデルクラスを作成するなどに利用することができ、非常に便利です。

こういった機能を利用していくとより適切な粒度で実装を行うことができるようになりますね。同じようなモデル実装についても、 ActiveSupport::Concern を利用して実装したモジュールをMix-inすることで機能を共通化することもできますし、よく考えられていますね。