保守しやすいコードにする為に永続化の処理でRails Wayからあえて外れた話
なぜそうしたのか、どうしたのか
Ruby on Rails(以下Rails)を使って作ったプロダクトを長年保守運用していく上で、辛くなる事の一つがcreateやupdate、save、destroyが様々な場所で行われ、いつどこでどんなデータが作成/更新/削除されているのかを把握することの難易度が高くなることです。
ActiveRecordのモデル外の様々な場所でattributeに代入され永続化されると、複雑化していくうちに追いきれなくなり影響範囲が分かりにくくなります。
grepしてもcreateやupdate、saveはそこかしこでヒットしますし…
そこで私達が開発しているSHOPCOUNTER Enterpriseでは、Rails wayからは敢えて外れ、下記のようなコーディング規約を作ってなるべく収拾がつくようにしております。
- create、update、saveをActiveRecordのモデル外では使わない
- 永続化の為のメソッドをActiveRecordのモデルに定義する
- 永続化の為のメソッドは業務で使う名前で定義する
- defaultの値やcallbackやdependentに頼らず、明示する
1. create、update、save、destroyなどをActiveRecordのモデル外では使わない
こちらは最初に書いた通り収拾がつかなくなることを防ぐ目的となります。
privateメソッド化したかったのですが、FactoryBotが動かなかった為、規約としてだけにしています。
ではどうやって永続化するのかは以降の項目で説明します。
2. 永続化の為のメソッドをActiveRecordのモデルに定義する
ここで他のクラスに公開されたインターフェース(データ操作用のメソッド)以外からはデータ操作しないようにしています。
モデル外でattributeに代入し保存などすると、カラム名が露出していることとなり、カラム名が変更されたりテーブルがを規化されて別テーブルに保存することとなっても影響範囲が閉じられ、モデルと別クラスが疎結合となります。
結果、凝集度も高まります。
3. 永続化の為のメソッドは業務で使う名前で定義する
メソッド名には業務で使っている操作の言葉(用語)を使う事で、その業務でどのような影響があるのか、振る舞いをするのかが、コードを読めば分かるようになり、自己記述的な(自己文書化された)コードとなります。
例: 「発行する」とか、「提出する」があれば、その通りにメソッド名(issue
とかsubmit
とか)をつける
※create
とupdate
はActiveRecordのメソッドで使われていて、自動で生えるメソッドもあるので、ぶつからないように業務用語で「作成する」だとmake〜
、「更新する」だとmodify〜
を使っていることが多いです
また、その業務上のルールに基づく入力値のチェック(バリデーション)は、このメソッド内に記述するようにしています。
ActiveRecordのvalidates
のif
やon
など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として切り出すかもしれませんが)。
ポップアップストアや催事イベント向けの商業スペースを簡単に予約できる「SHOPCOUNTER」と商業施設向けリーシングDXシステム「SHOPCOUNTER Enterprise」を運営しています。エンジニア採用強化中ですので、興味ある方はお気軽にご連絡ください! counterworks.co.jp/
Discussion