てめえらのRailsはオブジェクト指向じゃねえ!まずはCallbackクラス、Validatorクラスを活用しろ!

  • 1046
    いいね
  • 4
    コメント
この記事は最終更新日から1年以上が経過しています。

ちょっと煽り気味のタイトルにしてみましたが、Railsで開発する時は意識的にOOPに寄せないとオブジェクトの力が活かせなくなるよってことと、Railsが提供しているクラスの責務を分割することを支援してくれる機能について話をします。

ActiveRecordの性質

Rails開発においては、モデル層にロジックを書いてコントローラーは薄くしろ、というのはしつこく言われているので、概ね浸透してきていると思います。

それに加えて、最近私が結構しつこく主張しておきたいのが、モデル = ActiveRecordでは無いよ、ということです。

ActiveRecordは成り立ちから言うと、ロジックとDBへの永続化をまとめてカプセル化するアーキテクチャパターンから来ています。(詳しくはエンタープライズアプリケーションアーキテクチャパターンという書籍を読むと良いです)
この方法はロジックが複雑でない場合、つまりCRUD+αぐらいの時には非常に上手くいきます。
RailsのActiveRecordは非常に便利なのでそこそこの規模ぐらいのものを作ってる時には余り問題になりません。

しかし、規模が大きくなってくるとActiveRecordだけでは色々と辛い側面が出てきます。
テーブルと1:1という形でクラス構造が縛られる事により構造化に限界が発生し、一つのクラスに何でもかんでもロジックを記述していくことになります。
これによって生じる問題がファットモデルです。
モデル層をActiveRecordだけに頼っていると、いずれこの問題にぶち当たることになります。

これを解決するには地道にオブジェクト指向の基本に立ち返ること、上手くアクティブレコードから責任を分離する事が必要です。
ActiveRecordは開発の初期には非常に便利で問題も余り顕在化しないため、そこに囚われていると気付いた時にはえらくファットになってるという事が十分にあり得ます。

Rails謹製 ダイエット食品

Railsは良く考えられたフレームワークなので、もちろんそれ自体の中にもちゃんと責務を細かく分けるための仕組みを準備してくれています。
Railsが提供してくれる機能を理解し、モデルの責任を分割することでより良いモデルを作っていくことができます。その中でも代表的な機能が以下の4つです。

  • Callbackクラス
  • Validatorクラス
  • ActiveModelモジュール
  • 値オブジェクト (composed_of)

今回は、その中で忘れられがちな印象を受ける上の2つについて解説します。

Callbackクラス

ActiveRecord::CallbacksからActiveRecordオブジェクトのコールバック機能の使い方を参照してみます。

ActiveRecordオブジェクトのコールバックはこんな感じで記述するのが簡単な方法です。

class Subscription < ActiveRecord::Base
  before_create :record_signup

  private
    def record_signup
      self.signed_up_on = Date.today
    end
end

この様に自分のオブジェクトのパラメーター調整等であればこのクラスの責任と言えるし、簡単なのでシンプルにprivateメソッドにしておけば他から変な形で利用されることもなくなります。

しかし、もしある程度複雑な処理が関係してくる場合は、もう少し分割したくなってきます。
例えば、RailsのAPIリファレンスにあるような特定カラムを透過的に暗号化したい場合や、コールバックの動作に条件がいくつかある場合等です。
こうなってくると、コールバック処理自体にもテストコードを書きたくもなりますね。

その場合は、機能名を表現するクラスを用意してそちらに処理を記述します。
Railsではクラスに特定のメソッドを定義しておくだけで、任意のクラスをコールバック処理に組み込むことができます。

APIリファレンスのサンプルを見てみましょう。

class BankAccount < ActiveRecord::Base
  before_save      EncryptionWrapper.new("credit_card_number")
  after_save       EncryptionWrapper.new("credit_card_number")
  after_initialize EncryptionWrapper.new("credit_card_number")
end

class EncryptionWrapper
  def initialize(attribute)
    @attribute = attribute
  end

  def before_save(record)
    record.send("#{@attribute}=", encrypt(record.send("#{@attribute}")))
  end

  def after_save(record)
    record.send("#{@attribute}=", decrypt(record.send("#{@attribute}")))
  end

  alias_method :after_initialize, :after_save

  private
    def encrypt(value)
      # Secrecy is committed
    end

    def decrypt(value)
      # Secrecy is unveiled
    end
end

このようにbefore_saveafter_save等のコールバックに対応するメソッドをパブリックメソッドとして定義しておけばOKです。引数にはコールバックを呼び出す切っ掛けになったオブジェクトが渡されます。

一つのモデルから分離することで、汎用的な処理を複数のクラスで再利用できるようになります。

こういった記述をする利点は他にもいくつかあります。

まず、機能に明確な名前と境界が付くことです。
一つのモデルにまとめて記述されていると、色々なものの中に埋もれてしまい、どういう時に利用されるメソッドなのかはっきりせず後から変更する時に悩む要因になります。
境界が明確になっている事で、後から見ても用途を把握しやすくなります。

また、テストが容易になるのも利点の一つです。
コールバックの起動を単なるpublicメソッドの呼び出しとして行えるため、テストコードで検証する際に無駄なトリックを使う必要が無くなります。
また、このクラスはActiveRecordのオブジェクトに縛られずにテストする事ができます。
このクラスをテストするのに必要なものは、適切なインターフェースを供えたオブジェクトであれば何でも良いのです。RSpecのdoublemock_model等で、ダミーのオブジェクトは簡単に高速に準備することができます。
もし、最初のサンプルコードのようにモデル内のprivateメソッドとして定義していたり、直接ブロックを渡すような記述をしていた場合、ActiveRecordのオブジェクトを実際に保存して、状態の変化を確かめる、という面倒なテストが必要になってしまいます。そうなってくると他のバリデーションやコールバックが影響する可能性もあって非常に面倒です。(もしくはprivateメソッドを無理やり呼び出すとか)

このように、コールバックに別のクラスを利用するのは単なるクラス定義だけで良いので非常にお手軽で、上手く分類できるとモデルがかなりすっきりします。

Validatorクラス

コールバックとバリデーションは非常に似た記述方法を取ります。
そしてバリデーションにもコールバック同様、クラスを分割する仕組みがあります。

Validatorクラスを利用するにはvalidates_withメソッドとActiveModel::Validatorクラスを利用します。

例えば、会議室の利用予約機能を作るとして、予定が他の予定と重なったらバリデーションエラーになる、という仕組みを考えてみます。

# started_at: timestamp
# finished_at: timestamp
class Schedule < ActiveRecord::Base
  belongs_to :room
  validates_with MustNotOverlapValidator
end

class MustNotOverlapValidator < ActiveModel::Validator
  def validate(record)
    overlapped_schedules = Schedule
      .where(room_id: record.room_id)
      .where("finished_at > ?", record.started_at)
      .where("started_at < ?", record.finished_at)
      .where.not(id: record.id)

    if overlapped_schedules.exists?
      record.errors.add :base, "Schedule must not overlap on other schedules"
    end
  end
end

こんな感じで、ActiveModel::Validatorを継承したクラスを作りvalidateメソッドをオーバーライドします。
利用するモデル側からは、validate_withの引数としてValidatorクラスのクラス名を指定します。

Validatorクラスには一つ注意点があります。Validatorクラスは通常一度しかインスタンス化されず、そのオブジェクトをずっと使い回すことになります。変にインスタンス変数とかを利用するとずっと残ってしまうので利用する場合は、意図せず変更されないように注意しましょう。

ValidatorクラスもCallbackクラス同様、テストが容易になり責務がはっきりする利点があります。
ValidatorクラスはActiveModelの仕組みに依存しているので、バリデーション対象のオブジェクトがテスト時に必要になりますが、特定の検証ロジックを単体で呼び出してテストすることが簡単になりますし、境界がはっきりするのでモックを利用しやすくなります。

ちなみにテスト時にValidatorクラスのインスタンスを作る時には、コンストラクタに引数としてハッシュを渡しておく必要があります。これはActiveModel::Validatorクラスのコンストラクタが引数としてオプションの設定値を取るためです。

まとめ

こんな感じで、Railsが提供している機能を活用すれば、モデルの責務をより細かく分割することができます。
利用するカラム値や関連等を上手く抽象化すれば再利用性も高まっていい感じです。
ただ、何でもかんでも分割すれば良いというものじゃないのが難しい所です。
ロジックがに無軌道にあちこちに散らばってしまうと、それはそれで全体像を把握することが難しくなります。
大事なのは名前がちゃんと付けられるか考えるということです。(ネームスペースどうする、suffixどうする、とか大分悩ましいですが)
一つのクラスとして独立してテストコードを書きたいかどうか、というのも私の中では指標の一つとして活用しています。

サンプルコードだと良い使い所を上手く表現できないのですが、継ぎ足し継ぎ足し開発していると結構色々あるものなんですよ。
なので、覚えておいて損は無いんじゃないでしょうか。

他にもモデル層を整理する考え方やテクニックは色々あります。
この記事で紹介したようなコールバックという体裁を取るよりも、トランザクションを一つにまとめるサービスクラスという形を取って明示的に呼び出した方が柔軟で扱いやすくなる場合もあります。
Advent Calendarの予告コメントを見ると、そういった記事も出てきそうなので引き続きのAdvent Calendarに期待してます。

おまけ

今までRailsにpull reqがマージされたこと無かったんですが、この記事書いてたらCallbackのサンプルコードのバグに気付いたので修正してpull reqを送った所、速攻でマージされて初コントリビュートが出来ました。
Advent Calendarに感謝です。