副作用をどこに置くか問題
BuriKaigi 2026
2026-01-09
副作用とは
外部から観察可能な環境に変化を与える
副作用の大変なところ
処理順に意味が生まれる
コンテキストに依存する
状態に依存する
ロールバック時に挙動が恐る
=>暗黙知
例:ユーザー登録
メアドは小文字にする
ハッシュ化する
DB書き込み
ログイン履歴書き込み
メール送信
ログ記録
問題
責務が多すぎる
変更に弱い
再利用性が低い
可読性が低い
失敗時の挙動
メールが失敗するとユーザーは作成されるが、メールは送られず、ログも残らない
オブジェクトのライフサイクルにフックする
before_save、after_commitに寄せる
=> テスタビリティが低い
メール、ログ出力等をモック化しないといけない
OOPの原則に沿った改善方法
オブジェクトの存在条件を守る
他の関心にメッセージを伝える
オブジェクトの存在条件を守る
オブジェクトが存在するなら必ずこの状態である
契約プログラミングっぽい?
Railsのvalidationってオブジェクトの生成サイクルにはフックしないのでは?
ちょっと気をつけないといけない気がする
オブジェクトが生成されているからといって存在条件を満たしているとは限らない
クラスのライフサイクルにフックさせる
カプセル化
Invariant=存在条件
他の関心にメッセージを伝える
after_commitに書くこともできるが、暗黙知になったり、テスタビリティが下がる、例外的な対応が増えたときに辛い
YAGNI原則:必要になるまで機能追加しない
ライフサイクルにフックする場合
他の関心が少ない
今後増えない
チーム、システムが小さい
他のエンジニアが驚かない
=> イベントに着目する
PointがなくてもOrderは成立するが一緒に作成されてほしい
注文が完了したというイベントに注目する
例:checkoutクラス
複数のオブジェクトをcheckoutクラスで扱い、同一トランザクションにする
注文とポイント配布をする
懸念点:このクラスを利用しないと整合性を崩せる=>やや暗黙知になる
要求が増えたとき
メールを送信する
checkoutの中でメールを送る + 在庫を操作する
依存が増えた場合、DIP(依存性逆転の原則)を利用する
subscribers.each { |subscriber| subscriber.on_checkout(order, user) }
memo: この実装だと引数が異なるときにめんどくさそう
トランザクションに含めてはいけないケース
ロールバックできないもの
外部APIの実行(メール送信など)