「クラスの責務」を意識するようになった話
公開: 2025年12月17日
最近は設計してからレビューしてもらって、実装という流れでタスクをしています。(この限りではないですが)
私の関わっているプロジェクトではレイヤードアーキテクチャを採用していて、MVCは当然のこと、クラスの設計や責務の範囲は厳しくレビューしてもらいます。
Railsを触り始めて半年くらい経って、やっと「書ける」と「考える」が別物だと感じるようになりました。
まだ設計論を語れるほどではないですが実装していて「これはここに置きたくないな」と思う瞬間は、確実に増えてきています。
私がRailsで業務処理を設計するときに、何を基準にクラスを分けているかをサンプルを使って整理してみます。
設計する時にはNotionにシーケンス図を書くのですが、最近設計したものをかなりシンプルに抽象化して設計意図は残したサンプルを作りました。
sequenceDiagram
autonumber
participant Entry as 入口<br/>(Rake / Controller)
participant Orchestrator as 業務処理の統括<br/>(Application Service)
participant Validator as 入力チェック<br/>(Form object)
participant TX as トランザクション<br/>(Application Service内)
participant Domain as 業務処理<br/>(Domain Services)
participant Job as 非同期ジョブ<br/>(Sidekiq)
participant Worker as 実処理<br/>(Worker)
%% ------------------------------
%% 入口
%% ------------------------------
Entry->>Orchestrator: 処理を開始(input)
%% ------------------------------
%% 入力チェック
%% ------------------------------
Orchestrator->>Validator: 入力をチェック
alt 不正な入力
Validator-->>Orchestrator: エラー
Orchestrator-->>Entry: 処理失敗
end
%% ------------------------------
%% トランザクション
%% ------------------------------
Orchestrator->>TX: トランザクション開始
TX->>Domain: 業務データを作成・更新
Domain-->>TX: 作成結果
TX-->>Orchestrator: コミット完了
%% ------------------------------
%% 非同期処理(副作用)
%% ------------------------------
Orchestrator->>Job: 非同期処理を登録(IDs)
%% ------------------------------
%% Sidekiq
%% ------------------------------
Job->>Worker: 処理を実行
Worker->>Worker: 外部IO / 時間のかかる処理
Worker-->>Job: 完了
%% ------------------------------
%% 返却
%% ------------------------------
Orchestrator-->>Entry: 処理成功controllerやrakeに当たる処理の入り口部分です。
ここはほんとに出入り口としてしか使わず、それ以上の責務は持たせません。
薄く薄くを意識し、いわゆる Fat Controller を避けています。
ただ現実はデータ取得して返すだけだったらここで終わることはあります。
複数のモデルのデータを操作するとか業務ロジックが複雑で複数のサービスクラスを使うときはOrchestratorとしてのサービスを作成します。
validation
transaction
非同期実行のトリガー
これらの処理の順番や組み合わせを知っているのは「業務処理の統括」のレイヤーの責務だけにしています。
バリデーションするレイヤーはFormクラスを作成します。
ここで渡ってきたデータが業務ロジックに入って問題ないか、データのあるべき前提条件を満たしているかをバリデーションします。
トランザクションはOrchestratorの中で行います。
何がトランザクションの対象になるかはしっかり考えて、非同期処理や並行、並列の処理についてはトランザクションの外に出します。(DBロックされるので時間かかる系は含めない)
また、万一外したデータ操作が失敗した場合はすでに登録されているデータをどうするかは慎重に考えなければいけないと思います。
最近は論理削除で対応しました。
ここは「業務フロー」は知らないで成立するように、個別の業務処理だけに集中できるよう設計します。
例えば、商品の登録をするだけのクラスのようなイメージです。
永続化の詳細は意識せず1つの関心事に閉じるようにします。
時間がかかる(bulk_importや動画のアップロード等)操作は非同期処理などのside effectにしないとレスポンス返すまでの時間がかかりすぎてレイテンシーが上昇してしまいます。
非同期にする場合はOrchestratorからキューに入れるだけにして、Job 側には処理の流れを持たせません。
成功、失敗はユーザーにどう知らせるか考えないといけないです。
メールで通知する、slackで通知する、実行中、実行完了などのstatusの管理(カラムを用意しないといけないかも等)など、他にも考えること出てきますね、、
それら全て終わったらやっと全処理終了です!(非同期だったら待ってないけど)
この処理はどのレイヤーの責務か?悩むこともありますが、ModelやControllerははっきりわかってきて、これから設計に強くなりAI使って爆速実装していきたいです。
シーケンス図で細かくサービスクラスの名前や何をそのクラスでするかまで書いておけばAIに投げたら結構すぐに綺麗めなコード出てきて嬉しいです。
業務フローを知らないサービスクラスはほんとに使いまわしやすくて便利さも感じていますし、テストもしやすいので、運用し続けることに耐えられる設計を私も意識して仕事していきたいです。
