🛣️

保守しやすいコードにする為に永続化の処理でRails Wayからあえて外れた話

2024/05/22に公開

なぜそうしたのか、どうしたのか

Ruby on Rails(以下Rails)を使って作ったプロダクトを長年保守運用していく上で、辛くなる事の一つがcreateやupdate、save、destroyが様々な場所で行われ、いつどこでどんなデータが作成/更新/削除されているのかを把握することの難易度が高くなることです。

ActiveRecordのモデル外の様々な場所でattributeに代入され永続化されると、複雑化していくうちに追いきれなくなり影響範囲が分かりにくくなります。

grepしてもcreateやupdate、saveはそこかしこでヒットしますし…

そこで私達が開発しているSHOPCOUNTER Enterpriseでは、Rails wayからは敢えて外れ、下記のようなコーディング規約を作ってなるべく収拾がつくようにしております。

  1. create、update、saveをActiveRecordのモデル外では使わない
  2. 永続化の為のメソッドをActiveRecordのモデルに定義する
  3. 永続化の為のメソッドは業務で使う名前で定義する
  4. defaultの値やcallbackやdependentに頼らず、明示する

1. create、update、save、destroyなどをActiveRecordのモデル外では使わない

こちらは最初に書いた通り収拾がつかなくなることを防ぐ目的となります。
privateメソッド化したかったのですが、FactoryBotが動かなかった為、規約としてだけにしています。
ではどうやって永続化するのかは以降の項目で説明します。

2. 永続化の為のメソッドをActiveRecordのモデルに定義する

ここで他のクラスに公開されたインターフェース(データ操作用のメソッド)以外からはデータ操作しないようにしています。

モデル外でattributeに代入し保存などすると、カラム名が露出していることとなり、カラム名が変更されたりテーブルがを規化されて別テーブルに保存することとなっても影響範囲が閉じられ、モデルと別クラスが疎結合となります。
結果、凝集度も高まります。

3. 永続化の為のメソッドは業務で使う名前で定義する

メソッド名には業務で使っている操作の言葉(用語)を使う事で、その業務でどのような影響があるのか、振る舞いをするのかが、コードを読めば分かるようになり、自己記述的な(自己文書化された)コードとなります。
例: 「発行する」とか、「提出する」があれば、その通りにメソッド名(issueとかsubmitとか)をつける

createupdateはActiveRecordのメソッドで使われていて、自動で生えるメソッドもあるので、ぶつからないように業務用語で「作成する」だとmake〜、「更新する」だとmodify〜を使っていることが多いです

また、その業務上のルールに基づく入力値のチェック(バリデーション)は、このメソッド内に記述するようにしています。
ActiveRecordのvalidatesifonなどcontextで場合分けしていく事も考えましたが複雑化しがちなので、メソッド内に閉じ込めた方が良いとの判断しました。

validates側はDB等保存する側の都合でのルールとして最低限行うこととし、メソッド内ではビジネスルール上でのチェック、としてそれぞれ棲み分けています。

4. defaultの値やcallbackやdependentに頼らず、明示する

どのような振る舞いをするのかを明記することで、誰が読んでも分かるようにしたい為です。
削除についても、関連テーブルを削除するのはassociationの部分でdependentで定義すればわざわざ書かなくても消えますが、あえて記載して、そのメソッドを見ただけで影響範囲が分かるようにしています(どういった依存の関連性かもassociationの記述にも明示しておきたいのでdependentも書きます)。

サンプルコード

上記を踏まえたサンプルコードは下記となります。
単純な銀行口座システムのモデルを考えてみました。

class BankAccount < ApplicationRecord
  has_many :histories, class_name: 'AccountHistory'

  class << self
    def open!(name:, branch_id:)
      create!(
        name:,
        branch_id:,
        freezed_at: nil,
        number: SecureRandom.uuid
      )
    end
  end

  def deposit!(amount:)
    AccountHistroy.deposit!(account: self, amount:)
  end

  def withdrawal!(amount:)
    AccountHistroy.withdrawal!(account: self, amount:)
  end

  def freeze!
    # NOTE: stateや正規化して別テーブルで管理など別の形になっても、このメソッド内を修正するだけでよい
    update!(freezed_at:)
  end
end

class AccountHistroy < ApplicationRecord
  self.inheritance_column = :_type_disabled

  belongs_to :account

  enum type: { deposit: 0, withdrawal: 1}, _prefix: true

  class << self
    def deposit!(account:, amount:)
      create!(type: :deposit, amount:, account:)
    end

    def withdrawal!(account:, amount:)
      create!(type: :withdrawal, amount:, account:)
    end
  end
end

まとめ

記述量は増えRailsの良さが減りますが、これら強制力があると基本的にモデルを見れば良くなるのと、意図しない場所からの変更がされない為にビジネスのルールとデータのルールが基本的には常に整合性が取れるようになり、結果保守しやすいコードとなると考えています。

当然コード量が増えるとFat Modelとなりますが、許容しております(肥大化しすぎたら永続化部分だけConcernとして切り出すかもしれませんが)。

COUNTERWORKS テックブログ

Discussion