ジンジャー研究室

長めのつぶやき。難しいことは書きません。

再利用可能なコンポーネントはアンチパターン

言いたいこと

Webフロントエンド界隈で「コンポーネント」という言葉が蔓延していて、「再利用可能になるように設計すべきだ」という論調があるが、実際には本当に再利用可能である必要性があるまで、極力考えないほうが良い。YAGNIとも言う。

以下、現時点での考え。

ビューの階層化自体はOK

ここはReactの恩恵と言っても良い気がしていて、それまであんまり明言されて来なかった「ビューの階層化」について公式で説明している点がとても良くて、開発者全員がビューはツリーになってるよねというマインドで統一できた功績は大きいと思う。

再利用可能なコンポーネント

ビューはツリーでいいんだけど、それをコンポーネントと呼んでいるのでなんとなくDatePickerとかTextEditorみたいな汎用的なものを想像して、「アプリケーションの事情を知っていてはいけない」という気持ちになって疎結合に作りたくなってしまう。そうすると、次の2つの点で引数の数が増大する。

  • グローバルな「Model」を知っていてはいけないので、それまでmodelを渡せば良かったところが、model.isEditMode, model.saveEnabled, model.flags.colors, model.users, ... のようにどんどん増えていく。

  • グローバルな「Action」を知っていてはいけないので、onInputChange, onSubmit, onUserSelect, ... のような引数がどんどん増えて行く。

React界隈では以前からこれを「バケツリレー」と呼んでいて、辛いので避けるべきものだとされてきたが、とはいえやっぱり疎結合にした方が良いのではというジレンマが生まれる。しかし、この記事ではそれに真っ向から反対したい。

アプリケーションを知る・知らない

コンポーネントは粒度によって大まかに「アプリケーションを知っている(=再利用不可能)」「アプリケーションを知らない(=再利用可能)」に分けられる。個人的な感覚では、普通にアプリケーションを作ると前者が圧倒的に多い。YAGNIの一言で済むんだけど、再利用は幻想なのでやめるべき。

状態を持つ・持たない

「アプリケーションを知らない(再利用可能)」となると、副次的に導入されるのが「コンポーネントの状態」だ。例えば、DatePickerだと「いま何月何日が選択されているか」などという状態をアプリケーションに知られることなく管理して、必要に応じてイベントなりなんなりで引っ張り出して使ってもらうという設計になる。これもまた結果的に複雑化の原因になる。

疎結合のようで密結合

仮に理想的で疎結合なDatePickerが完成したとして、じゃあアプリケーションからそれを使いましょうとなった時に、問題が生じてくる。実は「権限に応じて選択できる部分とできない部分を変えましょう」とか、「ユーザーに応じて表示するカレンダーを変えましょう」という話になって、isSelectableとかformatDateみたいな引数が増える。ここまでは「バケツリレー」でお馴染みの辛さなのだが、もっと進むと「クリックした時にその日の予定一覧を取得してポップアップしましょう」とかいう話になる。いや、もちろんそれは汎用的に型付けして設計することは出来るので見た目は疎結合なんだけど、もうほとんど特定のアプリケーションのために存在しているだろう、という話になる。

重複するデータ

先ほどの例で、DatePickerがユーザの予定をキャッシュしていたとすると、他でもその予定を使いたいとなった時にデータが重複する。「ユーザの予定はDatePickerに保存されているはずだから、そこから取ってきて渡す」というのは明らかにおかしいし、グローバルに持っておくべきデータのはずだ。逆に、DatePickerが状態を持っていなかったとすると、今度は「DatePickerコンポーネントの使い方として、onFetchイベントが呼ばれたら外部で必ずキャッシュを保持してください」みたいな約束事を作ることになって、それも苦しい。

なぜ「コンポーネント」を作りたくなるのか

部分的なモデルとビューが「ほとんど」同じ関心を持っていることが多いのが原因だと思う。UIデザインとして、似たようなデータは似たような場所に表示するのがユーザとしてもわかりやすい。おそらく8割くらいモデルとビューが一致する。だから一緒にしてコンポーネントにするのだが、残り2割のために疎結合という前提が崩壊して辛い結果を生む。

共通化したいのは「見た目」

ほとんどの場合、共通化のモチベーションとしては「見た目を統一したい」だと思うので、振る舞いは二の次に考えよう。そうすると、与えたデータを表示するだけの「シンプルな関数」としてのビューが良いということになる。例えば、デフォルトボタンとプライマリボタンを共通化しておくとか、カレンダーも2画面以上で使うなら、ただカレンダーを描画する関数だけを用意しておく。

モデルは分割・アクションはグローバル

ただし、やはり「関心の分離」はしたい。そこで、もう少し具体的に考えると「引数に渡すのはある程度絞ったモデルで、アクションはグローバルなものを発行して良い」というルールが、今のところ一番バランス良く収まる気がしている。最初はアクションをツリー状にしてFooAction Foo.Clickみたいな感じにしていたのだが、結局その結果実行されるロジックはフラットだったので、本質的にはあまり意味がなかった。だからもう、アクションが30個並ぼうが50個並ぼうが気にしなくていい。本来そういう性質のアプリケーションなんだから、そういうものだと思えということ。「大きくなったから」という理由だけでアクションを分割したりモデルを分割したりすると、その部分の見た目は綺麗になるけど別の部分にしわ寄せが来るぞと。

型に任せてあとは禅の精神で書く

そもそもこれを書こうと思ったきっかけが、以下の記事。

medium.com

「デカいけどだから何?落ち着けよ」とのこと。自分も最近巨大なモデルとアクションに嫌気がさしてリファクタリングを試みたんだけど、上に挙げたような諸々の理由で失敗していた矢先にこの記事に出会ったので「ですよね」としか言えなかった。特にElmの場合は強力な型がついているので、見た目がおぞましくても意外と何とかなるし、後でリファクタリングしてもほぼバグらないので安心していて大丈夫。Elm Architectureがモジュラリティに優れていて素晴らしいという話と矛盾するようだけど、あれは本当にモジュラリティが必要な時のために取っておけばOK。

まとめ

再利用可能なコンポーネントは幻想であり、多くの場合アンチパターン。巨大なモデルやアクションはそれ自体悪いことではないので、恐れずそのまま突き進もう。あんまり賢くやることを考えすぎず、アプリケーションの本質を考えることに時間を費やそう。

以上。