実践ViewComponent(1): 現代的なRailsフロントエンド構築の心得(翻訳)
はじめに
GitHubのViewComponentライブラリは、Ruby on Railsアプリケーションのビュー層を構築中に開発者たちの頭が爆発しないために使われてきました。ViewComponentの人気は着々と上昇中ですが、まだViewComponentの実力に見合うほどの勢いではありません。
本記事は、皆さんがViewComponentをぜひ試してみる必要がある理由について2部構成で解説いたします。Evil MartiansがこれまでViewComponentを用いるプロジェクトで培ってきたいくつかベストプラクティスや、さまざまなヒントや裏技を検証します。
- 実践ViewComponent(1): 現代的なRailsフロントエンド構築の心得(翻訳) -- 本記事
- 実践ViewComponent(2): コンポーネントを徹底的に強化する(翻訳)
- 実践ViewComponent(3)TailwindCSSのクラスとHTML属性(翻訳)
パート1では、モダンなRailsアプリケーションのビュー層構築でコンポーネントによるアプローチを適用するときの高度な側面を中心に据えながら、荒野へと足を踏み入れてみたいとおもいます。また、コンポーネントは比較的新しく登場した手法で、採用率は今のところ比較的高くない(らしい)にもかかわらず、SPA構築の選択肢として優秀である理由についても学びます。特に、冒頭で述べたように、コンポーネントによるアプローチを(正気を失わずに)正しく適用する方法についても学べることが重要です。
このパート1では、全体像を見失わないためにコードは最小限にとどめています。しかし既に開発者としてベテランの域に達していて、設定の細かな部分以外には興味がなく、今すぐコンポーネントの荒野に踏み出しても構わないという方は、ご遠慮なく本記事をスキップして、現場で生まれたお望みのコードスニペットがたっぷり詰まったパート2をお読みいただけます😉
🔗 MVCの'V'に立ち返る🌱
しかし先に進む前に、まずは一歩下がって、皆さんの心をよぎっているであろう「なぜ?」を解決することにしましょう。「もう2022年だというのに、何が悲しくて今どき誰もやってなさそうなビュー層の構築方法を昔ながらのMVCアプリで苦労して学ばなければならないわけ?🤔」
まったくですよね。今の時代にやってくるプロジェクトは、別のフロントエンドチームが別のフロントエンドアプリをメンテナンスする形がほとんどです。フロントエンド開発はこの数年で急成長を遂げて成熟しています。昔なら想像もできないほど複雑なレベルでアプリケーションを構築できる時代です。しかし、それと引き換えにどうなりましたか?
はい、お察しのとおりです。メンテしなければならないアプリケーションが、フロントエンドとバックエンドの2本立てに増えてしまいました。単純な話ですね。油断すると開発コストはたちまち倍増し、書かなければならないコードも倍増するでしょう。エンジニアも今までの倍の人数を雇わねばならなくなり、結局ますます費用がかさむことになります。
(ここで述べた以外にも隠れたコストがあることをお忘れなく: インフラ費用の増加のようなわかりやすいコストもあれば、チームの運営やコミュニケーションを適切に保つのがますます困難になるといったわかりにくいコストもあります)
エンジニアが2種類になる→チームメンバーが増加する→開発コストが上昇する(時間も費用も)
コミュニケーションの隠れたコストを暴く愉快なイラスト(出典: Toggl)
バックエンド開発者
1: どうしていつも電球周りで問題が起きるわけ?
2: やべ、明かりをデータベースにデプロイし忘れてた
3: このまま黙ってフロントエンドのせいにしちゃえ
フロントエンド開発者
1: 電球が壊れてるべ
2: 黄色いペンキでも塗っておくべ
3: バックエンドには電球ついてるよって言っておくべ、でも今日のうちだけだべ
本当に適切な問いは「古典的なMVCを使うべき理由はあるのか?」ではなく、「古典的なMVCを使ってはいけない理由はあるのか?」だと思うんですが、いかがでしょう?😉
身近な事例で考えてみましょう。「そのプロジェクトでSPAは本当に必要か、それともオーバーキルか?」という問いかけです。
もっとましな問いは「古典的なサーバー駆動MVCを使ってはいけない理由はあるのか?」です。
モダンでレスポンシブなWebアプリケーションを構築するには、ReactやVue.jsやSvelteなどを使うしかないのでしょうか?もちろん、そんなはずはありません!
そのいい例は、実は皆さんのすぐ目の前にあります。GitHubです。現代のオープンソース開発を強力に支えているGitHubのアプリは、(SPAではなく)マルチページのRailsアプリケーションで、ほとんどのビューで今もERBテンプレートが使われています。皆さんがどう思うかはわかりませんが、これほど複雑なアプリが現在も絶え間なく成長と改良を繰り返しているにもかかわらず、GitHubは極めて信頼性が高く使いやすいWebアプリケーションのひとつだと私は思っています。
GitHubのような巨大アプリケーションが古典的なサーバー駆動MVCでうまくいっているなら、自分のアプリでもうまくやれるチャンスはあるのです!
現代は新しいツールを最初から使えるのですから、むしろGitHubの頃よりも有利であるとも言えるでしょう。たとえば、開発コミュニティでは最近"HTML over-the-wire"という手法が人気を博し始めています。HTML over-the-wireについてはPhoenixフレームワークのLiveViewが先陣を切り、そしてあのHotwireがRails界を席巻しました。Hotwireは、シンプルなレスポンシブWebインターフェイスならJavaScriptを1行も書かずに構築できることを約束します。もちろんHotwireは銀の弾丸ではありませんが、上で手短に説明した明らかなメリットに加えて、さまざまなプロジェクトで実際にSPAを構築できる新たな手段であることは間違いありません。
このあたりを詳しく学びたい方は、Vladimir Dementyevの良記事『Frontendless Rails Frontend』をぜひ一度ご覧ください。
ともかく、「特定のプロジェクトで必要になるのはSPAか、それとも昔ながらのマルチページアプリケーション(MPA)か」という問題には議論の余地があるものの、ひとつ確かなのは、SPAとMPAのどちらを選ぶにしても、コードをまともに構造化できるメンテ可能な手法が必要になるということです。これでやっと本題の✨コンポーネント✨の話に入ります。
🔗 コンポーネントのメリット 🌿
フロントエンド開発は既に「コンポーネント化」の歴史が長いので、そのことに誰も疑問を抱かないほどです。フロントエンドがコンポーネント化されている理由は明らかで、コンポーネントは「分離されている」「テストしやすい」「再利用可能」「コンポジション可能(当然!)」という優れたコードのあるべき姿を体現しているからです。
10年前に私たちが書いていたjQueryドリブンのスパゲッティ化コードの頃と比べれば、それまで馬に乗ったことしかない人がいきなりギンギンのデロリアン号で爆走するようなものです。このコンポーネントという先駆的なソリューション(つまりReactやVue.js)がたちまち業界を席巻して広く採用されたのも不思議ではありません。
しかしながら、今の時代にプロジェクトでrails new
するとどんなものが作られますか?パーシャルにビューヘルパー...これではまるで2005年にタイムスリップしたみたいです。この手法はコード品質の最も基本的な標準にも違反しているので、すっかり時代遅れになっていることは再三指摘されています。このパーシャルとビューヘルパーによるアプローチでも、jQueryスパゲッティのときと同じく「コードの頑固な癒着」「暗黙の引数」「予測できないデータフロー」「マジック定数」が引き続き諸悪の根源になります。こんなものをテストするなんて勘弁です。
このあたりについて詳しくは、Joel HawksleyによるRailsConfでの発表『Rethinking the View Layer with Components』をご覧ください。
単体テスト: ビューのコードが正しくテストされていないとこうなる(出典: Monkey User)
これがあなたの言う正しい振る舞いってことよね?
今の話が、かつてフロントエンド界隈がさんざん苦しんだ問題とまるっきり同じものに思えますか?はい、そのとおりです。違いがあるとすれば、私たちバックエンド開発者が業務の特性にかまけてこの問題から目を背けていたことです(作業がフロントエンド開発に取って代わられたせいもありますが)。しかし実際には、サーバーサイドでHTMLをレンダリングする古典的な手法に本質的な問題はありません(HTTPは文字通りHypertext Transfer Protocolの略なので、こんなことをわざわざ言うのも妙ですが)。
Railsのビュー層は決して「見込みなし」ではありません。必要なのは優れたツールだけです。
そこで登場するのがViewComponentライブラリです!ViewComponentは、これまで手に入らなかったRubyネイティブのコンポーネントパターン実装であり、上で述べた問題を100%解決するだけでなく、他にいくつもの問題を解決してくれます。ViewComponentの効果は絶大でありながら、コアとなるアイデアは信じられないほどシンプルです。すなわち、1個のビューコンポーネントは、ERBやSlimなどのテンプレートに関連付けられた単なるRubyオブジェクトなのです。
コンポーネントをレンダリングするのに必要なのは、インスタンス化することだけです。他のRubyオブジェクトと同様に、必要な依存関係を引数としてコンストラクタに渡してRailsの#render
に押し込めれば、それで完了です。
「それならRailsのパーシャルと大差ないんじゃないの?」パーシャルでやっていることをコンポーネント内部でも手動でやっているだけのように思えるかもしれませんね。しかしViewComponentではパーシャルではなく素のRubyオブジェクト(PORO)を採用したことで、これまで暗黙だったものが明示的になり、それによって多くのメリットが得られます。中でも最も重要なのは「予測が効くようになる」ことです(つまり本質的にメンテ可能になります)。Ruby最強の特徴であるOOPによってビューが最大のパワーを発揮するようになるのです。
しかしそれだけではありません。たとえば、テストにはどんな影響があるでしょうか?上述の定義を見れば、コンポーネントのテストはPOROのテストと同じぐらい簡単だと知ったら皆さんは驚くかもしれません(私の言葉が信じられない方はこの後のセクションをお読みください)
ViewComponentを使うことで、自分の書いたビューのコードにようやく自信が持てるようになり、大量のリクエストテストやシステムテストの穴を塞いで回る必要もなくなります。この種のテストは遅くて壊れやすいという悪評で知られ、しかも不十分です(詳しくは以下の動画"統合テストはサイテー"をどうぞ)。
つまり、これまで理論上しか存在しないとされていた伝説の「健全なテストピラミッド」に、現実に手が届くようになったのです。
そういえばコンポーネントのテストは信じられないほど高速という話がまだでしたね。ViewComponentのDOMのテストは、基本的にコストゼロです。それでもパーシャルから乗り換えたくないという方には、せいぜいどこかのビューの深い深いチェインの奥底で条件分岐のテストが成功しますように(そしてフォースとともにあれ!)、とお祈りするしかないでしょう。
GitHubのコードベースでは、ViewComponentの単体テストは同様のコントローラテストの100倍高速です。
もちろん、コンポーネントをバックエンドで利用するときの(すぐにはわからない)メリットは他にもいろいろあります。その中でも、ひときわ注目に値するのは以下だと信じています。
ビューコンポーネントを採用する最大のメリットは、バックエンドチームとフロントエンドチームが対等になることです。
フロントエンド開発者は「コンポーネントで考える」ことに慣れています。私の経験で言うと、バックエンドでも同様に「コンポーネントで考える」ようにすることで(率直に言えば、遅かれ早かれそうする必要が生じます)、ビューで発生した問題をバックエンド修正する必要が生じたときの学習曲線が劇的に緩やかになります。
面白いことに、ViewComponentが注目を浴びるずっと前からEvil Martianlsでは同様のカスタムソリューションを利用してきました。詳しくは以下の「Evil Frontシリーズ」記事をどうぞ。フロントエンドエンジニアとバックエンドエンジニアが共通の土台を見出すときの参考にもなります。
ご存知のように、最近のRailsのアセットパイプラインは(PropshaftやesbuildやViteなどのツールによって)全般的に大きく改善されました。機能も使い勝手もフロントエンドのツールに匹敵するようになったと言ってもよいでしょう。フロントエンド開発者と同じ土俵に上がるために唯一足りないのは、正しいデザインパターンです。そして私は、ViewComponentがついにそのギャップを埋めたと信じています。そして、バックエンドでビューコンポーネントを試す価値は間違いなくあることを納得いただけると思います。
🔗 ✨コンポーネント✨する方法 🌲
これで一般的な話についてはひととおりできたと思いますので、この辺でちょっと苦い薬を飲んでいただくことにしましょう。
残念ながら、コンポーネントを使いさえすれば問題が魔法のように解決するわけではありません。既存のビューコードの上にちょこっと盛り付けただけで改善されるとは思わないでください。改善するためのルールというものがあります。
コンポーネントは(残念ながら)この図のユニコーンのようなものではない(出典: Abstruse Goose)
AMラジオのしくみ:
- アンテナ
- チューナー
- 検波器
- 増幅器
- よしなに音を出してくれる魔法のユニコーン
アーキテクチャのどんなパターンでもそうですが、コンポーネントで実際にメリットを得るには、コンポーネントをどんなふうに操作するか(あるいはどんなふうに操作しないか)についてある程度の知識が要求されます。そうした知識なしでコンポーネントを導入してしまうと、レガシーコードがそのまま居座り続けるのはまだいい方で、下手をすると最悪の"小賢しい"レガシーコードになってしまうかもしれません。
ありがたいことに、バックエンドエンジニアに必要な知識は既に豊富に揃っています。これはひとえにフロントエンジニアの皆さんが10年以上に渡ってベストプラクティスを熱心に積み上げつつ、production環境でも数々のバトルテストを繰り返してベストプラクティスを鍛えてくれたおかげです。私たちがしなければならないのは、これらのベストプラクティスをひたすら現実に適用することです。車輪は既に発明されているのですから、後はその車輪で走り出すだけです。
フロントエンドとバックエンドでは、明らかに事情が異なる部分があります(だからこそ私はReactドキュメントにリンクしておしまいにせずに、こうして記事を書いているわけです)。しかしここで述べたどのベストプラクティスについても、背後にある考え方のコア部分は変わりません。そういうわけで、コードスニペットを読むときは一歩下がって目を細め、細部にとらわれずにトータルな考え方に集中することをおすすめしたいと思います。
必須というほどではありませんが、最初にViewComponent公式のハウツーガイドにざっと目を通しておくこともおすすめします。そうすることで、コードスニペットを読むのが少し楽になるでしょう。
この方法は、コンポーネント方式の背後にある原理原則の理解を深めるうえでも有用ですし、特にViewComponentのドキュメントに書かれていない"標準から外れた"いくつかの機能を利用するコードスニペットに出くわしたときに混乱を避ける効果もあります。詳しくはパート2ですべて解決されますのでご心配なく!今は全体像を把握することに集中しましょう。
🔗 ヘルパーではなく、実際の振る舞いをテストすること
テストについては、コンポーネントのテストはとっても美味しい🍰と先ほども書きました。それにしてもコンポーネントのテストはどんなふうに行えばいいのでしょうか?というか、そもそも何をテストすればいいのでしょうか?
答えは、他のRubyオブジェクトをテストするときと同じように、publicインターフェイスをテストします。コンポーネントの場合は(驚かないでくださいね)、コンポーネントのテンプレートをテストするのです。
コンポーネントのRubyクラスで定義されているメソッドに対してテストを書く方法は楽ですし、広く親しまれているので、ついそう書きたくなります。しかしよく考えてみれば、その方法は本質的にprivateメソッドに対するテスト(これは禁じ手です)と変わりません。そうしたメソッドは単なるヘルパーとして扱えば大丈夫です。
「なるほどわかった、でもテンプレートのテストってどう書くの?」よくぞお尋ねくださいました!
以下のコード例を見てみましょう。
<!-- app/views/components/menu/component.html.erb -->
<% if current_user %>
<div class="greeting">Hello, <%= current_user.name %>!</div>
<%= button_to t(".sign_out"), users_sessions_path, method: :delete %>
<% else %>
<%= button_to t(".sign_in"), users_sessions_path %>
<% end %>
皆さんはこのコードで何が見えますか?私には、いかにもテストでカバーしてくださいと言わんばかりの、条件ロジックを伴う「命令的(imparative)」なコードが見えます。
テンプレートは程度の差はあれ「宣言的(declarative)」なものであることが前提なので、ここで命令的という言葉を使うとちょっと戸惑うかもしれませんが、余分なものを取り払って考えれば、コア部分は私たちが普段書いている通常のコードと変わりません。コードが命令的なら、それに沿ってテストしなければなりません。
このコンポーネントのテストは以下のような感じになるでしょう。
# spec/views/components/menu_spec.rb
describe Menu::Component do
subject { page }
let(:component) { described_class.new }
before do
with_current_user(user) { render_inline(component) }
end
context "current_userがいる場合" do
let(:user) { build(:user, name: "Handsome") }
it "サインアウトボタンが表示される" do
is_expected.to have_link "Sign out"
end
it "あいさつ文が表示される" do
is_expected.to have_content "Hello, Handsome!"
end
end
context "current_userがいない場合" do
let(:user) { nil }
it "サインインボタンが表示される" do
is_expected.to have_link "Sign in"
end
end
end
なお、このマークアップのアサーションは厳密な形で書かれていませんのでご注意ください(今はそこに注目する意味はありません)。ここで注目したいのは、あくまで他の単体テストを書くときと同じ部分、つまり条件ロジックと計算の部分です(話を簡単にするため、ここでは単なる文字列の式展開にしています)。これが、ビュー層のテストカバレッジを改善する方法です。
ViewComponentは、コンポーネントを分離した形で素直にテストする方法を提供します。そのおかげで、最終的にビューで実際に信用できるコードを書けるようになります。
そして、これらのテストがどれほど爆速であるかをご覧ください。爆速である理由は、テスト対象が静的な出力であり、HTTPリクエストやブラウザの完全なセットアップが不要だからです。
「え、静的?じゃあJavaScriptの動的な振る舞いはどうやってテストするの?」おっしゃるとおり、今は静的でも、そのうちJavaScriptをコンポーネントに振りかけたくなる日が来るでしょう。
現時点のViewComponentのメンテナーたちは、実際にブラウザで動作するビューコンポーネントの動的な振る舞いを独立してテストしやすくなる機能をマージしようとしています(#1061、その後マージ済み)が、ViewComponentのプレビュー機能と昔ながらのシステムテストを使えば同じことができます。必要な作業は、以下のようにテストでプレビューページを読み込むことだけです。
# spec/system/components/my_component_spec.rb
it "does some dynamic stuff" do
visit("/rails/view_components/my_component/default")
click_on("JavaScript-infused button")
expect(page).to have_content("dynamic stuff")
end
🔗 グローバルステートの受け渡しには"コンテキスト"を使うこと
コンポーネントではレンダリングするデータが必要になりますが、そのデータはそもそもどこから来るのでしょうか?伝書鳩や(禁断の)グローバル変数のような難解なオプションを排除すれば、コンポーネントにデータを渡す方法は「引数」しか残りません。しかしデータはトップダウンで受け渡されるので、カレントユーザーのようなよく使われるデータについては相当厄介なことになります。実際のコンポーネントにデータが届くまでに、コンポーネントツリーのあらゆる階層でデータを手動で受け渡さなければならなくなるからです。
「ヤバい!どうやって修正するの?」フロントエンドに詳しい方なら答えをご存知でしょう。それがコンテキストです。
依存関係を明示的に注入する代わりに、依存関係を暗黙で注入する方法がコンテキストです。コンテキストとは、コンポーネントツリー上にあるどのコンポーネントからもアクセス可能な共有オブジェクトのことです。
方法のひとつは、dry-effectsを使うことです。このgemは、Ruby向けの"algebraic effects"の実装です。いきなり難解な用語が登場しましたがご心配なく。これは単に、コールスタック上のどこかにある値を設定して、以後どこからでもアクセス可能にするものです(例: コントローラでcurrent_user
を設定するとビューコンポーネントでアクセスできるようになる)。
パート2では、まさにこの方法を解説する予定です。とりあえず今は、こういう手法があるということと、乱用してはならないことだけは肝に銘じておいてください。コンテキスト経由で暗黙で受け渡されるものが増えれば増えるほど、データがどこから来たかを追いかけるのが難しくなり、テストでそうした点に配慮するのもつらくなります。
🔗 コンポーネントツリーのネストを深くしないこと
マトリョーシカ人形は「どこまで入れ子を繰り返せるんだろう」と考えるのが楽しいですよね。同様に、ネストしたコンポーネントツリーで作業するときにも、このネストはどこまで深くなるんだろうという疑問が持ち上がります(理性がカンペキに失われるまで、です)。傷口を無駄に広げないために、ネストが深くなる理由と回避方法について考えてみましょう。
私たちは既に、頻繁に使われるデータをコンテキストでコンポーネントツリーの下の方に渡す方法を知っていますが、すべてのデータをそうやって受け渡すわけではないことは明らかです。実際には、ほとんどのデータは引数で渡されるので、上のセクションで触れた問題はまだ残っています。
コンテキスト経由でのデータ受け渡しが適用できない状況で「引数の掘り下げ」問題を解決するには、データではなくコンポーネントを渡します。
ViewComponentは、そのためのさまざまな方法を提供しています。それがcontent
アクセサとスロットです。
コンポーネントでスロットを使う例を見てみましょう。
# app/views/components/feed/component.rb
class Feed::Component < ApplicationViewComponent
renders_one :pinned
renders_many :posts
end
<!-- app/views/components/feed/component.html.erb -->
<div class="pinned">
<%= pinned %>
</div>
<% posts.each do |post| %>
<%= post %>
<% end %>
コンポーネントを以下のようにレンダリングするとします。
<%= render(Feed::Component.new) do |c| %>
<% c.with_pinned do %>
<%= render(Post::Component.new(@pinned_post)) %>
<% end %>
<% @posts.each do |post| %>
<% c.with_post do %>
<%= render(Post::Component.new(post)) %>
<% end %>
<% end %>
<% end %>
コードがずいぶん増えてしまいましたね。以下のようにデータを引数で受け渡しすることにした場合と比べてみると、上のように書く価値はないと思われてしまうかもしれません。
<%= render(Feed::Component.new(pinned: @pinned_post, posts: @posts)) %>
しかしこんな状況を考えてみてください。
アプリケーションにフィードが2つ(個人単位とグローバル単位)あり、post
コンポーネントで両者の外見を少し変えたくなったとします(個人フィードでは投稿者名を非表示にするなど)。
もちろん、post
コンポーネントに新しいオプション(show_author
など)を追加する必要はありますが、feed
コンポーネントにも同じオプションを追加しておかないと、post
コンポーネントがレンダリングされたときにそのオプションがどちらに設定されているかをfeed
コンポーネントが認識できません。
これと同じことをツリー上のすべてのコンポーネントで繰り返さなければならないとしたらどうでしょう?どんなに冷静な作業者でもブチ切れ確定です。
子コンポーネントが親コンポーネントと異なるデータを持たなければならない場合は、ほぼ確実に、親コンポーネントのテンプレートにハードコードするのではなく、コンポーネントとして受け渡さなければならないことを意味します。
この手法は頭痛の種を大きく減らすのみならず、コンポーネントが再利用しやすくなるという効用もあります。たとえば、pinned
スロットに別のコンポーネントを表示したい場合や、スロットは同じままでfeed
コンポーネントを別のコンポーネント(board
など)に差し替えたい場合はどうすればよいでしょうか?今述べた方法なら、基本的にタダで手に入ります。
🔗 汎用性の高いコンポーネントを切り出すこと
フロントエンド界隈にしばらく身を置いたことがある方なら、「スマート(Smart)」コンポーネントや「ダム(dumb)」コンポーネントという言葉(あるいはプレゼンテーショナルコンポーネントとコンテナコンポーネント↓)を聞いたことがあるかもしれません。これらはそれぞれ「汎用コンポーネント」と「アプリ固有コンポーネント」と呼んでもいいでしょう。
参考: Presentational and Container components
呼び名はともかく、前者はアプリケーションで「パレット(pallet)」として振る舞うことが仮定されます(パレットはアプリケーションのデータモデルについては関知しません)。そして後者はこのパレットを用いて実際にビューをレンダリングします。
プレゼンテーショナルコンポーネントは表示を重視し、コンテナコンポーネントは振る舞いを重視します。
基本的に、あるコンポーネントにActiveRecord
オブジェクトを渡すと、間違いなくアプリ固有コンポーネントになります。
汎用コンポーネントとアプリ固有コンポーネントを分離することで、コンポーネントをアプリ全体で再利用する機会が増え、その過程でコンポーネントを「DRY」にする機会も増えます。
もう少しやれそうなときもあるんだけどね(出典: Monkey User)
再利用可能なコンポーネント
- task#1とtask#2を両方行う時間はないんだよね
- むむ、ひらめいた!
- 汎用の「再利用可能コンポーネント」をあげるよ
- 考慮しておきたいエッジケースもいくつかあるんだけどね
もしかするとこの分離にそこまでこだわる価値はないのかもしれませんが、UIの一貫性を維持するうえで役に立つので、アプリケーションでどのコンポーネントがコアなのかを考えてみることは重要だと思います。いつかそのうち、そのコンポーネントをオープンソースにできるかもしれません(GitHubがPrimer ViewComponentsでやっているように)。
🔗 単一責任の原則を守ること
数千行におよぶ神モデルからコードの臭いが立ち昇るように、数千行のビューテンプレートもコードの臭いを発します。テンプレートの場合は、実は数百行程度でも猛烈な悪臭を放ちます。その理由は、テンプレートの中にあるものすべてが少なくともどこかに親子関係でつながっていて、しかもそれで終わることはめったにないからです(これはクラス内のメソッドとは対照的です)。
おそらく皆さんも「癒着を減らし、凝集度を高くせよ」という言葉を聞いたことがあるでしょう(基本的には、突き詰めると昔ながらの「分割統治」に行きつきます)。これはモノリスのモジュラーアーキテクチャの話になると必ず登場します。
さて、皆さんはこの言葉をどうお考えですか?これはビューコンポーネントでも同じです。同じものをモジュールと呼ぼうが、ブロックと呼ぼうが、コンポーネントと呼ぼうが、手法のコア部分は変わりません。
何よりも、私たちがコンポーネントを使う理由は、コードベースにおけるコード品質の水準を底上げするためであることを忘れてはいけません。
つまり、コンポーネントでも普段と同じように原則を守る(少なくともそう努める)ことです。特に当てはまるのが「単一責任の原則」です。
よいプラクティスを守るかどうかはあなた次第(出典: xkcd)
- このプログラムのフローを再構築する手もあるけど、1箇所だけならgotoを使っちゃってもいいのでは
- 「よいプラクティスを守れ」か...どのぐらいマズいか試してみるか(
goto main_sub3;
をコンパイル)
真面目な話、コンポーネントの巨大なツリーはフロントエンドのコミュニティでも非難を浴びます。これにはれっきとした理由がいくつもあります。巨大ツリーは理解もリファクタリングも難しく、再利用はほぼ不可能です。時とともに最寄りのコンポーネントのロジックとの癒着が進んで実装がこじれてしまったら、コンポーネントのサブツリーごとゴミ箱に捨てて最初から書き直す方が早いこともあります。
コンポーネントはアトミックにすべきであり、単一責任のみに関心を寄せるべきです。
コンポーネントの巨大なツリーを分解する作業は時間も労力もかかりますが、複雑さを押し止めることが目的であれば💯%やる価値があります。
🔗 コンポーネント内でのデータベースクエリは避けること
これはバックエンド開発に特化した話です。
ビューはデータをレンダリングする場所です。データをフェッチする場所ではありません。
データのフェッチは、ビューではなくコントローラで行いましょう。
たとえ話ですが、皆さんはベッドで食事したりしませんよね?「してるけど何か?」なるほど、私もやってますが...っとそういう問題ではなく、気持ちよく暮らすには明確な関心の分離(separation of concerns)が必要だという話です。
ベッドでスナックを食べたら、そのたびに食べカスがポロポロこびりついてなかなか取れなくなってしまいますよね。ベッドの食べかすが迷惑なのと同様に、N+1クエリ問題も迷惑です。「ビューにデータベースクエリを書けばいいんじゃね?」と決定すれば、きっとN+1クエリ問題が発生します。N+1クエリ問題を回避するには、できるかぎり先手を打ってデータをプリロードしましょう。
P.S. ところでクッキーも食べカスがたくさん出ますが、アイスクリームなら大丈夫(これはコンポーネントのたとえ話でも何でもない、単にベッドで寝酒をするときのコツです)。
実は、さらなる予防策として、開発中はコンポーネントからのデータベースクエリを完全に禁止することすら可能です。そのためのセットアップについてはパート2で解説します。乞うご期待!😉
ふぅ〜!これでコンポーネント方式の採用を決めたときに待ち受けている厄介な落とし穴についてはほぼカバーできたと思います。これはあくまでケースバイケースであり、厳格なルールが存在するわけではありません。しかし一般的には、上で解説したガイドラインを守っておけば、今よりも快適に楽しく手軽にメンテナンスできるコードを得られることでしょう。
次回は、いよいよproduction環境でのViewComponent利用という野性味あふれる世界へと足を踏み入れて、ViewComponentを自分のものにします。
概念の話はこれでおしまいです。パート2ではいよいよコードを見ていきましょう!🌲🌳🌲🌳🌲
概要
元サイトの許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。図はすべて元記事からの引用です。
また、"Algebraic effects"や"Effects"はコンピュータサイエンスの概念で、定訳がないため英ママとしています。
参考: Rubyでもalgebraic effectsがしたい! - lilyum ensemble