はじめに
こんにちは。アメリカ在住で独学エンジニアを目指している Taira です。
前回の Query Object に続くリファクタリングシリーズです。
Rails の API 開発では、モデルの値をそのまま返すのではなく、
- 整形(通貨形式、日付形式など)
- ラベル化(enum → 表示文字列)
- ネスト構造化(関連モデルを JSON に含める)
が必要になる場面が多くあります。
これを各 Controller や View に散らばせると、レスポンス仕様の所在が不明確になり、保守性が下がります。
そこで使えるのがPresenterです。
Presenter とは?
Presenterは、
ドメインオブジェクト(モデル)の生データを、API レスポンスに適した形式へ変換する“変換レイヤー”
です。
MVC の中では以下のような位置づけになります。
Model → 生データ(ドメイン)
Presenter→ 出力用データ(変換・整形)
View/API → 最終的な描画や送信
- Model:ビジネスロジック・データ構造
- Presenter:表示や出力に適した形へ変換
- API:変換後のデータを返却
実装例
以下は PORO(Plain Old Ruby Object) といい、ActiveRecord などを継承しない Ruby のオブジェクトとして書いています。
Presenter
# app/presenters/teachers/index_presenter.rb
class Teachers::IndexPresenter
def initialize(result)
@current = result[:current]
@teachers = result[:teachers]
end
def as_json(*)
{
current_user: format_user(@current),
teachers: @teachers.map { |teacher| format_user(teacher) }
}
end
private
def format_user(user)
user.as_json(
include: {
students: {
only: %i[id student_code name status school_stage grade]
},
teaching_assignments: {
only: %i[id student_id user_id teaching_status]
},
class_subjects: {
only: %i[id name]
},
available_days: {
only: %i[id name]
}
},
methods: %i[last_sign_in_at current_sign_in_at]
)
end
end
Controller での使い方
# app/controllers/api/v1/teachers_controller.rb
class Api::V1::TeachersController < ApplicationController
def index
result = Teachers::IndexQuery.call(current_user, school: @school)
render json: Teachers::IndexPresenter.new(result).as_json, status: :ok
end
end
メリット
- レスポンス仕様を 1 か所に集約
- Controller がスリムになる
- Presenter は純 Ruby クラスなので単体テストが容易
- Jbuilder や Serializer を使わない軽量な実装が可能
他の手段との比較
手法 | 特徴 |
---|---|
Presenter | 純 Ruby クラス。依存を増やさず軽量に出力仕様を定義できる |
Jbuilder / RABL | テンプレート形式で定義。ビュー層寄り |
ActiveModel::Serializer | gem ベースで柔軟だが依存が増える |
まとめ
- Presenter は「モデルの生データを API レスポンス用に変換する責務」を持つ
- Controller で
Presenter.new(model)
を返すだけにすると仕様の集約が容易 - 単体テストで仕様を保証しやすく、保守性が高まる
💡 ポイント
「このエンドポイントのレスポンスはどこで定義しているの?」という質問に、
**「この Presenter です」**と即答できる状態を作ると、チーム開発でも強いです。