Timee Product Team Blog

タイミー開発者ブログ

Rails MVC を抽象化して捉える - 一貫性のあるクラス設計のために

こんにちは、タイミーでバックエンドのテックリードをしている新谷(@euglena1215)です。

Rails アプリケーションの開発をしていると、fat になってしまった Sidekiq worker や、ドメインロジックらしき実装が書かれている Serializer に遭遇したことがあるのではないでしょうか。

この記事では、MVC アーキテクチャを一段階抽象化して捉えることで、Rails のさまざまなレイヤーに対して一貫したプラクティスを適用する考え方を紹介します。ある程度 Rails を触ったことのあるエンジニアが感覚的に理解している概念を、改めて言語化したものです。

MVC の役割を改めて整理する

まず、MVC の各レイヤーの役割を整理しておきましょう。

  • Model: ビジネスロジック、データ、ルールそのものを表現し、データの保存・取得を行う
  • View: データをユーザーに返却するための適切な形で変換する
  • Controller: 外部からの入力を受け取り、Model を操作しデータの取得・更新を行い、View で変換したデータを外部へ出力する

ここで重要なのは、MVC を「Model, View, Controller という三つのディレクトリ」として捉えるのではなく、「三つの責務」として抽象的に捉えることです。

「実質〇〇」という視点

一番分かりやすい例は、JSON の組み立てを行う Serializer です。Serializer は app/serializers ディレクトリに配置されますが、その責務は「データを適切な形に変換してユーザーに返却する」ことであり、これは実質的に View です。

この「実質〇〇」という視点でタイミーの Rails アプリケーションの各レイヤーを分類してみましょう。

実質 Model

ディレクトリ 説明
app/models Model そのもの
app/policies 特に認可にフォーカスしたもの
app/validators 特にバリデーションにフォーカスしたもの

Policy や Validator は、ビジネスルールを表現するという点で、 Model の責務を担っています。

実質 Controller

ディレクトリ 説明
app/controllers Web API リクエストにおけるエントリポイント
app/workers 非同期処理におけるエントリポイント
app/mailers メール送信におけるエントリポイント
lib/tasks rake タスクにおけるエントリポイント
app/services エントリポイントから括り出された処理
app/forms エントリポイントから括り出された処理

ポイントは、Service*1 や Form*2 がエントリポイントではないものの、Controller に実装されるべき処理を括り出したものであるという点です。そのため、実質的には Controller と捉えるのが妥当です。

実質 View

ディレクトリ 説明
app/views HTML やメール文面の組み立て
app/serializers JSON の組み立て

Serializer を View と捉えることで、View に関するプラクティスを Serializer にも適用できるようになります。

この視点のメリット

「〇〇は実質的に Model / View / Controller である」という感覚を持つことで、MVC の一般的なプラクティスを MVC 以外のレイヤーにも適用できます。

例えば、以下のようなセルフチェックが可能になります。

一般的な MVC のプラクティス チェックリスト

  • 実質 Controller は十分に薄く、ドメインロジックや表示用ロジックが混ざっていないか?
  • ドメインロジックは実質 Model に寄せているか?
  • 表示のためのデータ加工は実質 View に寄せているか?
  • 実質 Model は実質 Controller に依存していないか?
  • 実質 Model は実質 View に依存していないか?

具体例

例えば、「Service クラスが肥大化している」という問題があったとします。

Service は実質 Controller なので、「Controller が肥大化している」と言い換えられます。Controller が肥大化する原因は、ドメインロジックや表示用ロジックが混入していることが多いです。

したがって、解決策は以下のようになります。

  • ドメインロジック → Model(または Policy, Validator)に移動
  • 表示用ロジック → View(または Serializer)に移動

このように、MVC のプラクティスを適用することで、自然と適切な設計に導かれます。

まとめ

  • MVC は「三つのディレクトリ」ではなく「三つの責務」として捉える
  • Rails の各レイヤーを「実質 Model / View / Controller」で分類することで、一般的な MVC プラクティスを適用できる
  • Service は実質 Controller、Serializer は実質 View、Policy は実質 Model
  • この視点を持つことで、一貫性のあるクラス設計が可能になる

日々のコードレビューや設計判断の際に、「これは実質どのレイヤーか?」と問いかけてみてください。

*1:タイミーでは、Service クラスをController や Sidekiq worker から呼ぶものと位置付け、Model からは呼び出されないようなルールにしています。詳細は https://tech.timee.co.jp/entry/2024/01/29/170000 をご覧ください。

*2:タイミーでは、Form クラスにおける明確なルールはないものの、特定のエンドポイントにおける処理を括り出したものという位置付けの使い方をしていることが多いため、こういった整理をしています。