普遍的な atom と molecule の境界を意識したコンポーネント設計

6 min read読了の目安(約5700字

はじめに

Dwango でニコニコ生放送のフロント開発を担当している misuken です。

AtomicDesign でコンポーネントを作る際 atom と molecule をどこで分けるかに悩むことが多いと思うので、今回は普遍的な atom と molecule の境界を意識したコンポーネント設計に関しての話をします。

この記事のスコープ

  • atom と molecule の境界のみにスコープを当てた内容になります
  • molecule と organism の境界には触れません(最後まで読むとその理由がわかります)
  • JSX で説明しますが、 React でも Vue でもこの記事の内容が有効です

悩ましい粒度の境界

AtomicDesign が話題になってから結構時間が経ちました。
しかし、実践してみたもののそのままだとうまくいかないところが出てくるという話を未だよく目にします。

大抵は AtomicDesign をそのまま実践することよりも、開発チーム内で共通認識を持てることを落とし所として、AtomicDesign を参考にチーム独自のルールを追加して運用していることが多いのではないでしょうか。

しかし、開発チームのメンバーごとにも捉え方の違いがあったり、チーム編成が変わったり、新しい案件でチームが変わったり、その都度折り合いを付けていくのは大変です。折り合いを付ける度に主観が衝突し、妥協が発生し、徐々にコンポーネントの秩序が失われていきます。

もっと幅広く共有できる指針があれば、開発はもっと楽になるのではないでしょうか。
ということで、普遍的な atom と molecule の境界を手に入れて、迷うポイントを減らしていきましょう。

本題

このような <button>push</button> HTML を出力するボタンのコンポーネントがあったとします。
これは誰もが atom に分類するでしょう。

しかし、アイコンと組み合わせたボタンの場合はどうでしょう。

<button><svg />push</button>
  1. IconButton として molecule にする
    • atom はこれ以上分解できない単位じゃないといけないので、アイコンを内包した時点で molecule になる
  2. IconButton として atom にする
    • Button を atom とする以上、他の要素を内包しても atom とし続ける
  3. Button に svg を渡せるようにするだけだから atom のままにする
    • Button 自体を拡張するだけで IconButton というコンポーネントは作らない

この時点で人によって粒度の解釈が変わり、問題が生じ始めます。

表現パターンと粒度

実際に開発を進めると、ボタンを以下のような表現パターンで使用したいシーンが頻繁に訪れます。
3つ目はアクセシビリティを高めつつ css の content: attr(aria-label); を使用してホバーツールチップボタンにする際に使用されます。

<button>push</button>
<button><svg />push</button>
<button aria-label="push"><svg /></button>

これら表現パターンの違いで粒度を変えるべきなのでしょうか?

あなたのプロジェクトのコンポーネントは探しやすいですか?

コンポーネントを探しやすいというのは「あのコンポーネントどこにあったっけ?」というときに、すぐ実装されている場所がわかるということです。

例えば IconButton コンポーネントを探そうとしたとき、以下のディレクトリまで来て、あなたは atoms と molecules のどちらに進みますか?

/atoms
/molecules

「チームの方針による」といったところでしょうか。

では、 CloseButton を探すときはどちらに進みますか?
このとき最悪のパターンは以下です。

  • 実装で svg を含んでいなかったら atoms
  • 実装で svg を含んでいたら molecules

つまり、実装に引きずられて粒度が変わるパターンを採用したら、コンポーネントは必ず探しにくくなるということを示しています。

  • 実装がわからないと場所を判断できない
  • 場所がわからないと実装を確認できない

これはデッドロックに陥った状態と言えます。

CloseIconButton にすればよいのでは?

CloseButtonCloseIconButton で分ければ多少マシかもしれません。
しかし、本当にアイコンの有無で atoms と molecules を分けたいでしょうか?
本心はそうではないはずです。

実際の開発中にありがちなシーンをイメージしてみましょう。

  1. 閉じるボタンをここに置きたいです
    • atoms/close-button/*
  2. 閉じるボタンにアイコンを付けられますか?
    • atoms/close-button/*molecules/close-icon-button/*
  3. やっぱりアイコン無いほうが良いので外してください
    • molecules/close-icon-button/*atoms/close-button/*

「ええと、今は閉じるボタンってどう実装されてたっけ・・・」

ちょっとした見た目の変更の度にコンポーネント名が変わったり、粒度が変わってディレクトリを行き来してしまっては、筋の良い方法とは言えません。

軽微な変更内容に対して、利用先でパスやコンポーネント名の変更が必要になるなど、破壊的変更のインパクトが大きくなりすぎてしまいます。

表現方法の違い

関心があるのは閉じるボタンであって、アイコンが付いてるかどうかは表現方法の違いでしかありません。表現方法の変更では破壊的変更が生じないようしたほうが明らかに得策です。

では、どのようにすれば変更に強いコンポーネントを作れるのかを考えてみましょう。

CloseButton を例にあげます。
以下の3つは表現は違えど、動作も意味も同じです。

<button>閉じる</button>
<button><svg />閉じる</button>
<button aria-label="閉じる"><svg /></button>

これらの違いは全て CloseButton が良しなに吸収してくれたほうが使いやすいコンポーネントであると言えます。

さらに突き詰めると、 "Close" の部分に依存しているのはアイコンとテキストの内容だけなので、値とレイアウトに分離し Button の標準機能で吸収できることがわかります。

// 実装イメージ
const closeButtonProps = { symbolIcon: <svg />, text: "閉じる" };
// 同じ props を渡し、レイアウトの指定だけで表現方法を切り替える
<Button layout="textOnly" {...closeButtonProps} />
<Button layout="default" {...closeButtonProps} />
<Button layout="ariaLabel" {...closeButtonProps} />

こうすることで、 Button コンポーネントが表現の違いを吸収する機能を提供するようになり、表現の違いによる破壊的変更の発生を防げます。

その他の表現パターン

Button と同様の表現パターンは Anchor や Item(メニューの項目) といったテキストを表現する系のコンポーネントで広く共通する部分です。

シンボルとなるアイコンだけでなく、以下のような表現が必要になる場合もあります。

  • ショートカットキーの表示
  • リスト項目の右端に表示される > などの項目選択時のアクションを表すアイコン

これらも基礎的なコンポーネントで吸収することで、粒度を安定させることができます。

粒度の境界

ここまでの説明を踏まえ、改めて粒度の境界を探ってみましょう。

表現パターンの違いで内包する要素が現れたとしても、それは粒度に影響を与えないということがわかっているので atom と molecule の境界がどこなのかを表現するとこうなります。

内包する構成要素が無ければ意味を成さないコンポーネントかどうか です。

molecule の例

構成要素を持つことが前提で、内包する構成要素が無ければ意味を成さないコンポーネントは molecule になります。

例えば、 List や Form といったコンポーネントは molecule になります。

出力する HTML が <ul></ul><form></form> だけの状態なら atom と言えなくもないかもしれませんが、実際に利用する際は必ず内包する構成要素が存在するので List や Form は molecule です。

Button は常に atom

Button の場合はアイコン等の要素を内包する場合がありましたが、それは表現の違いです。内包する構成要素が無くてもボタンとしての意味を成しており、その点で molecule とは明確な違いが存在します。

これはボタンが主体のコンポーネントなら、中に何が入ろうがそれは atom の条件を満たしていることを表し、それらのコンポーネント名の末尾は常に Button で終わるということを意味します。

画像のボタンの例

例えばテキストの代わりに画像 <img src="foo.png" alt="push" /> が表示されたとしましょう。

画像に文字が書いてあるかもしれませんし、画像にイラストが描いてあるかもしれませんが、画像の内容によって粒度が変わるのは不自然です。テキストの文字列で表示しようが、画像の文字列で表示しようが、伝えようとしていることやボタンとしての機能に変化はありません。

やはりこれらは表現の違いでしか無く、本質的にはただのボタンであると言えます。

普遍的な粒度の境界

これらを踏まえると、普遍的な Button = atom の関係が定まっていると言えるでしょう。

他の全てのコンポーネントに対しても同じ方法で整理できるので、コンポーネント名の末尾を見れば、それが atom か molecule かを判断できるようになります。

このようにコンポーネントの末尾に UI の型を表す名前を明示する手法は BCD Design で有効な手段であることがわかっているので、是非参考にすると良いでしょう。

改めて問います

以下のディレクトリ構造で CloseButton はどっちにありますか?
これまでの内容を理解していれば明確に atoms にあることを説明できます。

/atoms
/molecules

これがわかりやすさであり、個人の主観や特定のチームに依存しない普遍的な atom と molecule の境界であると言えるでしょう。

粒度は重要なのか?

ここまで、 AtomicDesign の粒度の話をしてきましたが、実はコンポーネントの粒度はそこまで重要ではないとも感じています。

概念軸で分類する BCD Design を使用すると、粒度軸で得られるメリットは依存関係の明確化くらいしかなく、概念軸で得られる関心の凝集のメリットのほうが圧倒的に大きくなるためです。

もちろん、より精度を高めていく上では粒度軸も正確であることに越したことはないのですが、多くの場面では概念軸で整理すると、さらに粒度軸で整理ほどの複雑度が残らないので概念軸だけで済んでしまいます。

AtomicDesign は molecule と organism の分類が曖昧になりやすい問題もあり、 atom と molecule を見極められるようになった先にも困難が待ち受けます。

BCD Design は organism 不要で AtomicDesign で抱えるモヤモがスッキリ解消できるため、より本質的な解決を目指す場合は "概念軸" を学んでみてはいかがでしょうか。

もっと奥の深い粒度の話

TextBox SelectBox ComboBox などでもっと奥深い粒度の話を掘り下げられるのですが、長くなったので今回はここまでにしておきます。

2021-04-14 意外と単純ではない TextBox が atom である理由 を公開しました。