← 前へ: Phase 4.2 | 目次 | 次へ: Phase 4.4 →
実行前に共通セットアップと各Phaseの確認手順を確認してください。演習コードはwork/で前のPhaseから引き継ぎます。
このフェーズでは、Phase 4-2で作成したルータの次のステップとして、Controller / Action 構造 をTDD(テスト駆動開発)で導入します。
Phase 4.2では、ルータによって
- HTTPメソッド
- パス
に応じて処理を振り分けることができるようになりました。
しかし現状では、ルータの行き先は多くの場合
- Proc
- ラムダ
などの無名の処理になっているはずです。
このままアプリケーションが大きくなると、
- 関連する処理が散らばる
- 同じ種類の処理をまとめられない
- 共通処理を入れる場所がない
- アプリケーションコードを書く場所が曖昧になる
という問題が起きます。
このフェーズでは、それを解決するために
「関連する処理を Controller という単位でまとめる」
構造を導入します。
なお、このフェーズでは必ず
RED → GREEN → REFACTOR
の流れで進めてください。
以下を説明できる状態になること:
- Controller とは何か
- Action とは何か
- なぜ Proc ではなくクラス + メソッドにするのか
- Router と Controller の責務の違い
- なぜ ControllerBase が必要になるのか
- Router から Controller / Action を呼び出せる
"users#index"のような指定を扱える- Controller から params を参照できる
ControllerBaseに共通処理を集約できる- TDD で設計を進められる
- Phase 4.2(ルーティングのTDD実装)完了
- Minitest を使ってテストを書けること
このフェーズでは、以下を必ず守ってください。
まず、Router の行き先として Proc ではなく Controller を使いたい、という失敗テストを書きます。
def test_routes_to_controller_action
router = Router.new
router.get "/users", to: UsersController.action(:index)
status, _, body = router.call(env_for("/users"))
assert_equal 200, status
assert_equal ["Users Index"], body
endclass UsersController
def index
[200, {}, ["Users Index"]]
end
endUsersController.action(:index)がまだ存在しない- 失敗してよい
👉 これが RED
UsersController.action(:index)が呼べるようにする- Router からその handler を実行できるようにする
- 最初はベタでもよい
- まだ汎用的でなくてよい
- まずは1つの Action が動けばよい
次に、同じ Controller に複数の Action を持たせます。
def test_routes_to_different_actions_in_same_controller
router = Router.new
router.get "/users", to: UsersController.action(:index)
router.get "/users/show", to: UsersController.action(:show)
status1, _, body1 = router.call(env_for("/users"))
status2, _, body2 = router.call(env_for("/users/show"))
assert_equal 200, status1
assert_equal ["Users Index"], body1
assert_equal 200, status2
assert_equal ["Users Show"], body2
end- まだ複数 Action を自然に扱えないなら失敗する
👉 RED
ここで、よりフレームワークらしい記法を導入します。
def test_routes_with_controller_action_string
router = Router.new
router.get "/users", to: "users#index"
status, _, body = router.call(env_for("/users"))
assert_equal 200, status
assert_equal ["Users Index"], body
end"users#index"をどう解釈するか"users"をどうUsersControllerに変換するかindexをどう呼ぶか
- まだ文字列指定を解決できないはず
👉 RED
"users#index"を分解する- 対応する Controller クラスを見つける
- Action メソッドを呼ぶ
- 文字列操作
capitalizeconst_get
- 最初から複雑な命名規則に対応しなくてよい
- 今必要な範囲だけでよい
Controller を導入した以上、Action の中から request 情報を扱いたくなります。
def test_controller_can_access_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
def show
[200, {}, [params["id"]]]
end
endparamsがまだないので失敗するはず
👉 RED
- Controller 初期化時に
envあるいは request 情報を渡す - Controller の中で
paramsを参照できるようにする
initialize(env)にするかinitialize(req)にするか- params をメソッドで返すか
ここで複数 Controller を追加してみてください。
UsersControllerPostsController
- 毎回
initializeやparamsを書いていないか - 共通処理が重複していないか
「Controller の共通処理をまとめるベースクラスが必要」
class ControllerBase
end
class UsersController < ControllerBase
def show
[200, {}, [params["id"]]]
end
endControllerBaseにparamsを持たせたい- 子クラスから利用できるようにしたい
- まだ何もないので失敗する
👉 RED
- request / params の初期化を
ControllerBaseに集約する - 各 Controller は継承するだけでよい状態にする
ここまで来ると、Controller の呼び出し手順が見えてきます。
例えば:
- Controller を生成する
- Action を呼ぶ
- 戻り値を Rack レスポンスにする
この一連の流れを整理してください。
actionクラスメソッドに閉じ込めるかdispatchインスタンスメソッドを作るか- どこまでを Router にやらせるか
Router が Controller の詳細を知りすぎないようにすること
- 存在しない Controller 名
- 存在しない Action 名
- 例外にするか
- 404にするか
- どの層で扱うか
※ この時点では簡易な実装でよい
最低限、以下を含むこと:
- Controller Action を呼び出せる
- 同一 Controller の複数 Action を扱える
"users#index"形式を扱える- Controller から params を参照できる
ControllerBaseに共通処理を集約できる
- Router
- ControllerBase
- サンプル Controller
以下について説明してください:
- Proc ではなく Controller が必要な理由
- Router と Controller の責務の違い
ControllerBaseを導入した理由- テストによって設計がどう決まったか
- RED → GREEN → REFACTOR を守れているか
- テストが仕様として機能しているか
- Controller の責務を理解しているか
- Router が Controller の詳細を抱え込みすぎていないか
- 共通処理の抽出が適切か
- テストの粒度の相談
- Rubyの
const_getなどの確認 - 責務分離の壁打ち
- 実装の生成
- 完成コードの出力
- 解答の丸写し
→ まずは1つの失敗テストから始めること
→ Router は「行き先を呼ぶ」ことに集中する
→ 重複が見えてから導入すること
→ テストから必要性を引き出すこと
このフェーズで得るべき理解は、
フレームワークとは、 「アプリケーションコードをどこに、どう書かせるか」を定義する仕組みである
ということです。
Controller の導入は、単にクラスを増やすことではありません。
それは、
- 関連する処理をまとめ
- 共通処理を差し込めるようにし
- アプリケーションの構造を規定する
ための重要な一歩です。
Controller / Action 構造ができると、次の問題が見えてきます。
- Action の中で毎回 Rack レスポンス配列を書くのがつらい
- redirect をどう表現するのか
- HTTP の詳細が Action に漏れている
次のフェーズでは、これを解決するために
render / redirect
を導入します。
Controller を導入すると、アプリケーションは「動く」だけでなく、構造を持ち始めます。
ここでは完成度よりも、
- どんな失敗テストを書いたか
- どの順序で抽象化したか
- 何を共通化すべきだと気づいたか
を重視してください。