← 前へ: Phase 4.5 | 目次 | 次へ: Phase 4.7 →
実行前に共通セットアップと各Phaseの確認手順を確認してください。演習コードはwork/で前のPhaseから引き継ぎます。
このフェーズでは、Phase 4.5までで構築してきた自作Webアプリケーションフレームワークに対して、Request / Params の扱いを TDD(テスト駆動開発)で整理します。
ここまでで、以下の要素はすでに揃っているはずです。
- Routing
- Controller / Action
- render / redirect
- View / Template
しかし現状では、Controller の中で扱う入力情報がまだ曖昧なはずです。
例えば:
- query parameter はどこから取るのか
- path parameter はどこにあるのか
- form parameter はどう扱うのか
paramsは何を含むのかrequestはどこまで Controller に見せるべきか
このフェーズでは、それらを整理し、
Action から request / params を自然かつ一貫した形で扱えるようにする
ことを目指します。
なお、このフェーズでも必ず
RED → GREEN → REFACTOR
の流れで進めてください。
以下を説明できる状態になること:
- Request とは何か
- Params とは何か
- query parameter / path parameter / form parameter の違い
- なぜそれらを Controller から統一的に扱いたくなるのか
- なぜ
requestとparamsを分けるのか
- Controller から
requestを参照できる - Controller から
paramsを参照できる - query parameter を
paramsで扱える - path parameter を
paramsで扱える - form parameter を
paramsで扱える requestとparamsの責務の違いを説明できる- TDD で request / params の仕様を積み上げられる
- Phase 4.5(View / Template のTDD実装)完了
- Minitest を使えること
このフェーズでは、以下を必ず守ってください。
最終的には、以下のようなコードを書ける状態を目指します。
class UsersController < ControllerBase
def show
id = params["id"]
page = params["page"]
method = request.request_method
render_text "id=#{id}, page=#{page}, method=#{method}"
end
endただし、最初からここを一気に作ってはいけません。 必ず小さな失敗テストから順に進めてください。
まずは、Controller の Action から request オブジェクトを扱いたい、という要件を固定します。
def test_controller_can_access_request
router = Router.new
router.get "/info", to: "pages#info"
status, _, body = router.call(env_for("/info"))
assert_equal 200, status
assert_equal ["GET"], body
endclass PagesController < ControllerBase
def info
render_text request.request_method
end
endrequestがまだ存在しないなら失敗するはず
👉 これが RED
- ControllerBase に
requestを持たせる Rack::Requestあるいは独自の Request ラッパーを通じて扱えるようにする
- まずは
Rack::Requestをそのまま持つか - 後で独自 Request クラスに包むか
最初から独自実装に寄せすぎなくてよいです。 まずは Controller から request が見えることを優先してください。
次に、URL のクエリ文字列を Action から扱いたくなります。
def test_controller_can_access_query_params
router = Router.new
router.get "/search", to: "pages#search"
status, _, body = router.call(env_for("/search?keyword=ruby"))
assert_equal 200, status
assert_equal ["ruby"], body
endclass PagesController < ControllerBase
def search
render_text params["keyword"]
end
endparamsがまだない、または query parameter を見ていないなら失敗するはず
👉 RED
- ControllerBase に
paramsを追加する - まずは query parameter だけを返せればよい
以下を説明できるようにすること:
requestとparamsは何が違うか- なぜ
request.paramsをそのまま使わず、ControllerBase にparamsメソッドを置くのか
Phase 4.2 で routing 時に path parameter を扱いました。 ここで、それを Controller 側から自然に触れるようにします。
def test_controller_can_access_path_params_from_params
router = Router.new
router.get "/users/:id", to: "users#show"
status, _, body = router.call(env_for("/users/42"))
assert_equal 200, status
assert_equal ["42"], body
endclass UsersController < ControllerBase
def show
render_text params["id"]
end
end- path parameter が
paramsに入っていないなら失敗するはず
👉 RED
- ルータが抽出した path parameter を Controller 側に渡す
paramsの中で query parameter と path parameter を統合して扱えるようにする
params["id"]で取れることの利便性は何か- path と query を統合することで何が楽になるか
- 逆に、分けて持つ価値はあるか
統合した以上、両方が同時に存在するケースを扱う必要があります。
def test_params_contains_both_path_and_query_params
router = Router.new
router.get "/users/:id", to: "users#show"
status, _, body = router.call(env_for("/users/42?page=3"))
assert_equal 200, status
assert_equal ["id=42,page=3"], body
endclass UsersController < ControllerBase
def show
render_text "id=#{params["id"]},page=#{params["page"]}"
end
end- 片方しか取れていないなら失敗するはず
👉 RED
- query parameter と path parameter を1つのインターフェースで扱えるようにする
- 最小実装でテストを通す
競合時の優先順位を考えてください。
例:
- path に
id=42 - query に
id=99
このとき params["id"] をどうするか
ここも正解は1つではありません。 どのルールにするかをテストで固定すること が重要です。
ここまでで GET リクエスト由来の parameter は扱えました。 次に、POST フォーム由来の parameter を扱います。
def test_controller_can_access_form_params
router = Router.new
router.post "/users", to: "users#create"
env = env_for("/users", method: "POST", params: { "name" => "Alice" })
status, _, body = router.call(env)
assert_equal 200, status
assert_equal ["Alice"], body
endclass UsersController < ControllerBase
def create
render_text params["name"]
end
end- form parameter を見ていなければ失敗するはず
👉 RED
- POST された form parameter を
paramsに含める - query / path / form を一貫した形で扱えるようにする
以下を説明できるようにすること:
- query parameter と form parameter は何が違うか
- それでも
paramsにまとめたくなる理由は何か
request を導入した意味は、params 以外の情報も扱えることにあります。
def test_request_exposes_path_and_method
router = Router.new
router.post "/inspect", to: "pages#inspect"
status, _, body = router.call(env_for("/inspect", method: "POST"))
assert_equal 200, status
assert_equal ["/inspect|POST"], body
endclass PagesController < ControllerBase
def inspect
render_text "#{request.path}|#{request.request_method}"
end
endrequestが単なるparamsの入れ物になっているなら不十分
👉 RED
requestはリクエスト全体を表すparamsは入力データをまとめたもの
という構造になるように整理する
Controller から見たときに、
requestはメソッド・パス・ヘッダなどの窓口paramsは入力値の窓口
として理解しやすい状態にすること
ここで、params の使い勝手を明確にしておきます。
- キーが存在しないとき
nilを返す - 文字列キーで扱う
- シンボルキーは扱わない、あるいは扱う
def test_params_are_accessed_with_string_keys
router = Router.new
router.get "/search", to: "pages#search"
status, _, body = router.call(env_for("/search?keyword=ruby"))
assert_equal ["ruby|"], body
endclass PagesController < ControllerBase
def search
render_text "#{params["keyword"]}|#{params[:keyword]}"
end
endここも正解は1つではありません。 使い方のルールをテストで固定すること が重要です。
👉 RED
- Step 13 で決めた仕様に従って実装する
- なぜそのルールを選んだのか説明できるようにすること
Action の中で次の2つを比較してください。
request.params["keyword"]params["keyword"]- どちらが Action にとって自然か
- なぜ ControllerBase に
paramsを生やしたくなるのか - フレームワークとしてどちらを推奨したいか
ここで気づくべきことは、
Action から見たときに「よく使うもの」は近くに置きたくなる
という点です。
ここでは、まだ独自 Request クラスを本格導入しなくても構いません。 ただし、必要性が見えるところまでは進めます。
Rack::Requestをそのまま Controller に見せ続けてよいか- 自作フレームワーク独自の path parameter との統合はどこでやるべきか
- framework 全体で request の仕様を固定したい場面はないか
「将来的には独自の Request オブジェクトで包みたくなる」
この気づきがあれば十分です。
ここまで進むと、次の責務が見えてくるはずです。
- env から request を作る
- ルータから path parameter を渡す
- query / path / form を統合して params を作る
- Controller から扱いやすい API にする
責務を整理し、必要ならメソッドを分けてください。
build_requestbuild_paramsmerged_paramspath_paramsrequest_params
- Controller は request / params を自然に使える
- ルータや ControllerBase に責務が偏りすぎていない
- 今後独自 Request クラスに進化させやすい形になっている
以下のケースをテストで定義してください。
- path parameter と query parameter が同じキーを持つ
- query parameter と form parameter が同じキーを持つ
- どの順序で上書きするか
- なぜその順序にしたか
例:
user[name]=Alice- いま扱うべきか
- 後続フェーズに回すべきか
- なぜか
以下のような情報を request から見たくなるケースを考える。
User-AgentCookieAccept
- params と違って、なぜこれらは
request側なのか
最低限、以下を含むこと:
- Controller から
requestを参照できる - query parameter を
paramsで扱える - path parameter を
paramsで扱える - query + path parameter を同時に扱える
- form parameter を
paramsで扱える requestが path / method などを持つ- params のキー仕様がテストで固定されている
ControllerBase- 必要に応じた Request / Params 補助コード
- サンプルController
以下について説明してください:
requestとparamsは何が違うか- なぜ query / path / form を
paramsにまとめたのか - parameter の優先順位をどう決めたか
- いま独自 Request クラスを本格導入しない理由
- テストによって設計がどう変わったか
- RED → GREEN → REFACTOR を守れているか
- テストが振る舞いの仕様として機能しているか
- request と params の責務を分けて説明できるか
- query / path / form を一貫して扱えているか
- 優先順位やキー仕様が曖昧になっていないか
- 過剰設計になっていないか
- Rack::Request の仕様確認
- query / form parameter の違いの確認
- テスト粒度の相談
- 責務分離の壁打ち
- 実装の生成
- 完成コードの出力
- 解答の丸写し
→ まずは Controller が自然に使えることを優先する
→ params は入力値、request はリクエスト全体
→ ルータと Controller の境界で責務を整理すること
→ 競合時のルールはテストで先に固定すること
→ フレームワークとしての使いやすさを意識すること
このフェーズで得るべき理解は、
フレームワークの仕事は、 バラバラな入力経路を、アプリケーション開発者にとって一貫した形に整えること
ということです。
request と params の整理は地味に見えますが、
ここが曖昧だと、フレームワーク全体の使い勝手と責務分離が崩れます。
逆にここが整理されると、
- Action は自然に書ける
- 入力値の扱いが一貫する
- 後続のフォーム処理やバリデーションにもつながる
という大きな効果があります。
ここまでで、自作フレームワークにはかなり基本要素が揃ってきました。
- Routing
- Controller / Action
- render / redirect
- View / Template
- Request / Params
次に自然に見えてくる問題は、
- ログ出力を毎回書きたくない
- 例外処理を各所に書きたくない
- 認証や共通前処理を差し込みたい
次のフェーズでは、これを解決するために
共通処理(Middleware / before_action 的な仕組み)
を導入します。
このフェーズでは、派手な画面の変化は少ないかもしれません。 しかし実際には、フレームワークの使い心地を大きく左右する重要な部分を扱っています。
大切なのは、
- どの入力経路をどう統合したか
- どの責務を request に置き、どの責務を params に置いたか
- それをどのテストで先に固定したか
を理解することです。
ここを丁寧に設計できると、 自作フレームワークは一段と「人が使えるもの」に近づきます。