テンプレート検索を例外が出ない非例外系メソッドへ分割
非例外系の #find を導入し、find_all(...).first のパターンを排除しました。これにより、テンプレート探索時に例外を捕捉する必要がなくなり、将来的な高速検索実装への足掛かりができます。
背景
従来、ActionView のテンプレート探索は lookup_context.find_all(...).first に依存しており、結果が無い場合は nil が返り、呼び出し側で rescue して例外処理を行うケースが散在していました。find_all は全候補を取得してから最初の要素を選ぶため、不要なソートや配列生成が発生し、パフォーマンスに余計なコストが掛かっていました。この PR は 例外を投げない検索 (#find) と、例外を必ず投げる検索 (#find!) を明確に分離することで、コードベースをシンプルにしつつ将来の最適化を容易にする狙いです。
技術的な変更
LookupContext に非例外系 find と例外系 find! を追加
lookup_context.rb では既存の find を 非例外系 として残し、新たに find! を実装しました。find! は内部で normalize_name と detail_args_for を呼び出し、@view_paths.find! に委譲し、見つからない場合は MissingTemplate を即座に送出します。これにより、呼び出し側は成功時はテンプレートオブジェクトを受け取り、失敗時は例外で即座に制御フローが抜けます。
@@
- def find(name, prefixes = [], partial = false, keys = [], options = {})
+ def find(name, prefixes = [], partial = false, keys = [], options = {})
@@
- alias :find_template :find
+ # non‑raising lookup used by most rendering paths
+ alias :find_template :find
+
+ def find!(name, prefixes = [], partial = false, keys = [], options = {})
+ name, prefixes = normalize_name(name, prefixes)
+ details, details_key = detail_args_for(options)
+ @view_paths.find!(name, prefixes, partial, details, details_key, keys)
+ end
PathSet に find! を実装し、エラーメッセージを統一
path_set.rb では find! メソッドを追加し、find の結果が nil の場合に MissingTemplate を発生させます。エラーメッセージは従来と同等の情報量を保持し、テストでも同様の出力が検証されています。
@@
- def find(path, prefixes, partial, details, details_key, locals)
- find_all(path, prefixes, partial, details, details_key, locals).first ||
- raise(MissingTemplate.new(self, path, prefixes, partial, details, details_key, locals))
+ def find(path, prefixes, partial, details, details_key, locals)
+ find_all(path, prefixes, partial, details, details_key, locals).first
end
+
+ def find!(path, prefixes, partial, details, details_key, locals)
+ find(path, prefixes, partial, details, details_key, locals) ||
+ raise(MissingTemplate.new(self, path, prefixes, partial, details, details_key, locals))
+ end
呼び出し側のコードを find / find! に置換
-
etag_with_template_digest.rbのpick_template_for_etagはlookup_context.findに変更し、例外が不要なケースで非例外系検索を利用。 -
digestor.rbのfind_templateはfinder.findに置き換え、シンプル化。 -
layouts.rbのデフォルトレイアウト検索ロジックはlookup_context.findに統一。 -
partial_renderer.rbとtemplate_renderer.rbでは、必ずテンプレートが存在すべき場面でfind!を使用し、失敗時は即座に例外を上位へ伝搬させます。
@@
- @lookup_context.find_template(options[:template], options[:prefixes], false, keys, @details)
+ @lookup_context.find!(options[:template], options[:prefixes], false, keys, @details)
@@
- @lookup_context.find_template(layout, nil, false, keys, details)
+ @lookup_context.find!(layout, nil, false, keys, @details.merge(formats: formats))
新しいフォーマット判定メソッド any_formats?
LookupContext に any_formats? が追加され、指定フォーマットが一つでも存在すれば true を返すようになりました。template_renderer.rb のレイアウト解決ロジックで、フォーマットが見つからないケースを明示的に判定できるようになり、冗長な例外処理が削減されています。
@@
- def any?(name, prefixes = [], partial = false, keys = [])
+ def any?(name, prefixes = [], partial = false, keys = [])
exists?(name, prefixes, partial, keys)
end
alias :any_templates? :any?
+
+ def any_formats?(name, prefixes = [], partial = false, keys = [], options = {})
+ exists?(name, prefixes, partial, keys, **options, formats: default_formats)
+ end
テストの更新
lookup_context_test.rb では find! を用いた例外メッセージ検証に変更し、期待されるエラーテキストが変わらないことを確認しています。また、PathSet の find! に対するテストも追加され、例外発生時のメッセージが正確であることが保証されています。
設計判断
例外系と非例外系の明示的分離
find と find! の二分割は、検索結果の有無に応じた制御フローを呼び出し側で明示的に選択できるようにする設計です。従来は find_all(...).first の結果が nil になるか、MissingTemplate がスローされるかで判定していましたが、ロジックが散在しやすく保守性が低下していました。この分割により、成功時は静かにオブジェクトを取得し、失敗時は即座に例外で通知という責務が明確化され、コードベース全体で一貫したエラーハンドリングが可能になります。
後方互換性の維持
find のシグネチャは変更せず、既存の呼び出しは全て非例外系へ流れるようリファクタリングしました。find! は新規メソッドとして追加されたため、既存コードが影響を受けることはありません。テストスイートの全パスが通過していることから、既存機能の動作は保持されていることが確認できます。
将来の最適化への土台
非例外系 find が「ベストマッチ」だけを返す実装に置き換えられる余地が生まれました。現在は内部で find_all の結果の先頭要素を取得していますが、将来的にインデックス検索やキャッシュ戦略を導入すれば、余計なソートや配列生成を回避できるようになります。今回の分割は、そのような最適化を導入しやすくするための設計的な布石となります。
まとめ
この PR はテンプレート検索ロジックを 例外系 (find!) と非例外系 (find) に分割し、呼び出し側が意図に沿った検索手段を選択できるようにしました。any_formats? の追加でフォーマット判定が簡潔になり、テストも更新されたことで既存の挙動は保持されています。結果としてコードの可読性と保守性が向上し、将来的な検索最適化への道が開かれました。