椅子に甘えないと心に決めて最近はスタンディングメインで仕事してます小宮山です。
実は私はペアプロ・モブプロ好き人間です。なぜ好きかというと、単にワイワイコードを書けるというのもありますが、何よりもそのときに考えていることをリアルタイムに共有できるからです。
メドピアのCTO室フロントエンドグループ(最近正式にグループ化されました)は、CTO室という何やら凄そうな名前の部署に所属している通り、メドピア社内のフロントエンド開発を幅広く支援するという役割を持っています。その一環としてペアプロ歓迎ムードを漂わせているわけです。
そして先日久しぶりにペアプロに誘われたのでほいほい承って参戦してみて、やはりペアプロという場はいいなと感じてこんな記事を書いています。
で、何をテーマにするかというとタイトルの通りです。おそらく近頃のフロントエンド開発に慣れた方なら特に意識しなくともそういう考えをしているのではと思うので、それほど目新しく斬新な考え方というわけではありません。
ただ世の中の開発者全員が全員、近頃のフロントエンド開発に慣れているわけでもないはずで、特に普段はバックエンドメインで片手間にフロントエンドも触るけどよく分からんという方もいるのではと思います。私は近頃のフロントエンド開発に慣れた側に立てているだろうということを最上の謙虚な心を持って認めると、残念ながら慣れていない方が感じている、何が分からないか分からないという気持ちを汲むのはなかなか難しかったりします。
なので役に立つのかは分からないけれども、もしかしたら誰かの役に立つかもしれないということで、どんなことを考えながらコードを書いているを紹介してみようというのが今回の趣旨です。
実装タスク
概要
- 既にテーブル形式でデータ一覧を表示する機能がある
- そのテーブルにて、行を選択できるようにしたい
- 複数行選択を可能にしたい
- 表示されている行全ての選択を切り替える全選択機能も欲しい
要するにこれをこうしたいというタスクです。選択して何をしたいんだということは気にせずいきます。
1stステップ - 機能の境界を意識する
選択して何をしたいんだということは気にせずいきます。
さらっと書きましたが実はこれも考え方として重要かもしれないということで1stステップです。例えば大元の要件が「まとめて選択して削除したい」という場合、「まとめて選択して削除する」機能と考えるのではなく、「まとめて選択する」機能と「削除する」機能を分けて考えた方がいいケースがほとんどです。
せっかくなのでなぜかを説明すると、今後新たに「まとめて選択して移動したい」という要望が来ても対応しやすいからです。そしてフロントエンドという領域にDBはないので、「削除する」「移動する」という機能はそれぞれAPIに処理を委ねることになります。ではフロントエンド側に何が残るのかというと、「まとめて選択する」という機能と、「選択したものをAPIに投げる」という機能です。APIに投げるのは多少のインタフェース調整が必要かもしれませんが、実質ただの関数実行です。つまり「まとめて選択する」という機能さえ作れてしまえば、「まとめて選択して○○したい」という要望の大半は叶ったようなものです。
以上を踏まえて、選択して何をしたいんだということは気にせず、「まとめて選択する」という機能をこれから実装しようと一目散に考えます。
2ndステップ - 状態から考え始める
選択して何をしたいかは気にしませんが、「選択して何かをする」が控えていることを忘れてはいけません。もし本当に「まとめて選択する」という機能だけが欲しいのであれば、テーブルに<input type="checkbox />"
をまぶした時点でもう実装は完了ということになります。
ここでふと思ったのですが、もしかしたらjQuery時代であればこれは正しかったのかもしれません。なぜなら「選択する」という機能はチェックボックスを設置するだけで実際に満たされるからです。そしてチェックボックスのDOMをそれぞれ取得して選択状態を調べてその後の「何かをする」に引き渡せば終了です。「全選択」機能はもう少し追加の実装が必要になるものの、まぁ適当にイベントハンドラを設定して適当な処理を適当に書けばおそらくなんとかなるでしょう。
当然ですが現在は(少なくとも観測範囲内では)jQuery時代ではないのでこういう考え方はしません。
「選択して何かをする」が控えている
スタート地点はここです。処理的にはゴール地点ですが設計的にはスタート地点です。選択した後に、その選択したという状態を必要とする後続処理が控えています。つまり、「選択したという状態」が欲しいわけです。
「選択したという状態」がある。ここが全ての基点となります。
何度でも言いますが「選択したという状態」が基点です。「選択する」という機能は二の次です。とりあえず機能を作り出すのではなく、真っ先に状態を定義します。
3rdステップ - 状態を形にする
基点となる状態が見えてきたのでそろそろ手を動かします。先に状態以外も含めた全体像を設計しきってしまうというのもありですが、ペアプロ想定ということで手を動かして抽象度を下げていきます。
「選択したという状態」を表現します。今回選択対象となるデータはそれぞれユニークキーid
を持っていると想定します。特段難しく考えるまでもなく、選択したデータのid
をArray
かObject
で持てば良さそうです。ぱっと見シンプルな気がするのでArray
でいきましょう。どちらでも大した違いはないのでお好みで。
let selectedIds:number[] = []
主役が完成しました。結局のところ、後続の処理に回すために興味がある情報はこれだけです。
型が付いている方が視認性が良いと思うのでTSで書きます。JS原理主義者のみなさんごめんなさい、私はTSに屈しました。
ちなみですがパフォーマンスをシビアに求めるならObject
方式がオススメです。
let selectedIds:{ [key: string]: boolean } = {}
そこまでのシビアさが求められるケースはあまりないのでお好みでどうぞ。あるいはAPIがどちらを採用しているかで判断するのがいいかもしれません。
4thステップ - 状態を変化させる
4rdと書きたい気持ちを抑えて4thステップです。
主役となる状態が作れたので、次はその状態に対してどんな操作をしたいか考えます。今回の操作はシンプルで、「選択する」と「選択を外す」です。2種類の操作ということですが、どちらの操作を行いたいかは状況によって変わります。状況とは、上で定義した状態のことです。
改めて説明するまでもなく、「選択済」なら「選択を外す」、そうでなければ「選択する」が実現したいことです。
function toggleSelected(selectedIds:number[], id: number): number[] { if (TODO 選択済) { return selectedIds.filter(_id => _id !== id) } else { return [...selectedIds, id] } }
特定フレームワークに依存しない考え方がテーマなので、なるべく簡素に書いていきます。
「選択済」かを判定して「選択を切り替える」関数を作りました。ただどうやら関数を完成させるには、「選択済」かの判定が必要なようです。ではそれはどうすれば得られるのか。もちろん、主役である「選択したという状態」から求めることができます。
function isSelected(selectedIds:number[], id: number): boolean { return selectedIds.includes(id) }
「選択を切り替える」関数も完成させます。
function toggleSelected(selectedIds:number[], id: number): number[] { if (isSelected(selectedIds, id)) { return selectedIds.filter(_id => _id !== id) } else { return [...selectedIds, id] } }
この時点で、「選択されたという状態」、「選択済」かの判定、「選択を切り替える」という操作が揃いました。なんだかもう選択機能が作れたような気がしてきませんか?気のせいではないです。個別の選択機能はもうこれで完成です。まだUIがないだけです。
5thステップ - 状態をUIと繋げる
選択するための状態とその状態を切り替えるための関数は既に作成しました。あとはそられをUIと連携させれば完成なのですが、この連携というのがなかなか厄介です。というのはかつての話で、今はそれほど厄介ではありません。
なぜ厄介ではなくなったかというと、それは昨今のフロントエンドに関わる方なら耳にタコができるくらい聞かされたであろう宣言的なUI構築を売りにした各種フレームワークのおかげです。宣言的というのが重要です。だからこそ先ほども状態ありきで処理を作っていました。そして状態に関する処理を既に作ってしまったので、あとはフレームワークのお作法に沿ってUIを組み立てていくだけです。細かく気を使う箇所は都度あるにせよ、状態さえ定まってしまえば、宣言的なUIの構築は最早シンプルな作業です。
というわけでここから先は単に今まで作ったものをフレームワークと合わせて組み立てていくだけなので、特筆すべきことはあまりありません。
というわけで終わりますと投げっ放しにする度胸もないので続きます。せっかくなので今回はみなさん大好きSvelteでいきます。
先に完成形を張ってしまいます。公式サイトにさくっと触れる砂場を用意してくれているので、そこでも遊んでみてください。
<script> const dataList = [ { id: 1, name: 'メドピア1号' }, { id: 2, name: 'メドピア2号' }, ] let selectedIds = [] $: isAllSelected = selectedIds.length === dataList.length function toggleAllSelected() { if (isAllSelected) { selectedIds = [] } else { selectedIds = dataList.map(data => data.id) } } </script> <p> 選択中: {selectedIds.join(', ')} </p> <table> <thead> <tr> <th><input type="checkbox" checked={isAllSelected} on:change={toggleAllSelected} /></th> <th>ID</th> <th>Name</th> </tr> </thead> <tbody> {#each dataList as { id, name }} <tr> <td><input type="checkbox" bind:group={selectedIds} value={id} /></td> <td>{id}</td> <td>{name}</td> </tr> {/each} </tbody> </table>
悲しいお知らせがあります。まずはパッと見てもらえれば分かるように、さすがに砂場だとTypeScriptは非対応でした。心の目で型の補完をお願いいたします。
さらに悲しいお知らせが続きまして、なんと先ほど意気揚々と作成したisSelected
とtoggleSelected
という関数は出番がありませんでした。
<input type="checkbox" bind:group={selectedIds} value={id} />
なんとこれだけでSvelteがチェックボックスと配列の双方向バイディングを完成させてくれました。選択判定やトグル処理は自作する必要すらなかったです。
せっかく書いたコードが不要になりましたが、無駄な時間を過ごしたと嘆く必要は全くありません。むしろその不要なコードを順を追って作ったからこそ、フレームワークが提供してくれる機能に対してより深い理解が得られるというわけです。たとえcommitログに何も残らなくともすべてを血肉に変えていきましょう。そしてペアプロ・モブプロに慣れてくるとcommitの量なんて無意味です。より大事なのはコード品質です。
もしSvelteにこんな便利な機能がなかった場合にどういう展開にしようとしてたかを一応解説しておきます。
<input type="checkbox" checked=選択されている? onchange=選択をトグルする />
正しいHTMLになっていませんがイメージとしてはこのような形です。選択状態の判定とトグル処理は既に用意したので、あとはこの形を各々のフレームワークでどう表現するか探すだけです。ここから先はフレームワークの表面的な作法や記法の問題です。
まとめ
かなり単純化してしまいましたが、コンポーネントの実装に取り組むときの流れは大概このような流れです。早く見た目を作りたい欲はぐっと抑え、とにかく真っ先に状態を設計して形に落とします。状態さえ定めてしまえば後はUI実装を思う存分楽しむだけです。
レビューだけではどうしても完成した後のコードを見るというケースが多く、こうした実装の考え方はなかなか共有が難しいものです。一方でペア・モブプロをしてみると、まさにこういった実装者の思考と共にコードを追っていくことができます。
ペア・モブプロが好きな理由もこのあたりだったりします。他人の作業風景を覗き見て取り入れることで自らの作業効率を改善できるという副次作用もあったりします。普段そういった機会があまりない方々も、ここまで読んでいただけた縁と思ってぜひとも周囲の方を誘ってペア・モブプロを試してみてください。
そしてなんと、メドピアではエンジニアを絶賛募集中です。集合知というキーワードの元にチーム開発を楽しみたい、そんな方は是非ともお声がけください。
メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!
■募集ポジションはこちら
https://medpeer.co.jp/recruit/entry/
■開発環境はこちら