この記事は第2のドワンゴ Advent Calendar 2018 24日目の記事です。
ドワンゴでニコニコ生放送のWebフロントエンジニアをやっています、 @misuken です。
- 14日目の React + TypeScript における ViewComponent の美しい合成技術
- 18日目の 超簡単に数値を 1,234 1.2万 12.3万 123万 1,234万 1.23億 ... に整形する方法
- 23日目の AtomicDesign の atom より小さな世界の扉を開く
に続いての投稿です。
今回のお題
今回は AtomicDesign でハマりがちなコンポーネントの粒度問題、ならびにコンポーネント名やディレクトリ構成の問題を解決するLayerdAtomicDesign(階層化アトミックデザイン) という考え方を紹介します。
AtomicDesign に階層の概念を取り入れると、様々な問題が解決できるというお話です。
これまでの問題
AtomicDesign を採用したプロジェクトを進めていて、以下のような問題を感じたことはないでしょうか?
- コンポーネントの名前に一貫性がない
- atoms と molecules の分類が曖昧
- molecules と organisms の分類が曖昧
- atoms molecules organisms のそれぞれで統一感のないコンポーネント名が並んでいる
- ライブラリ的なコンポーネントとアプリケーションに依存したコンポーネントが同じディレクトリに混ざって探しにくい
- なんだかんだで再利用性が低くてコンポーネント数が増えるすぎる
問題の核心
- 参考にできる命名規則が確立されていない
- 粒度の分類方法が確立されていない
- 階層という概念が抜けている
参考にできる命名規則が確立されていない
命名がうまく管理できていないというプロジェクトなどを見ると、
大体コンポーネントの内容がコンポーネント名に正確に反映されていないことが多いように感じます。
コンポーネントの名前には ドメイン 概念 アクション 機能 が深く関係しています。
そして、これらは命名するのではなく、法則に従って当てはめることで精度の高い名前を得ることができます。
今回の記事では、コンポーネントの名前の法則性について細かく紹介していきます。
ちなみに、命名規則という呼び方は名前を生み出すような印象を与えるので、あまり適切な感じがしていません。
命名法則という捉え方をしたほうが自然な感じがします。
コンポーネントが持つ名前は文脈や構成、役割によって導き出されるものだからです。
命名はクリエイティブな作業ではなく写像と考えるのが吉。
粒度の分類方法が確立されていない
最も厄介なのは molecules と organisms の分類でしょう。
世の中的にもいくつかの目安は出ているものの、未だはっきりしないという方は多そうです。
次に、 atoms と molecules でも迷う方も多いです。
これに関しては分類方法が確立されています。
23日目の記事 で「アイコンを含んでいても、アンカーを含んでいても、日付を含んでいても見出しは見出し」という話をしています。
機能を軸に考えれば理解できます。
他には @rhirayamaaan さんの Atomic Designってデザイナーには難しくない!?という話 という記事もとてもわかり易い解説をしています。
今回の記事では、organisms を今までとは別の視点から分類する方法を紹介します。
階層という概念が抜けている
atoms molecules organisms のそれぞれで統一感のないコンポーネント名が並んでいる
ライブラリ的なコンポーネントとアプリケーションに依存したコンポーネントが同じディレクトリに混ざって探しにくい
なんだかんだでコンポーネント数が増えて再利用性が低い
これらが発生する理由というのは、名前付けの問題という場合もありますが、
多くの場合は抽象度の違うコンポーネントが同じディレクトリに混在することで発生します。
コンポーネント名の先頭に来る単語が階層ごとに整理されていれば解決する問題です。
今回の記事では適切な階層の境界がどこに存在するのかを紹介します。
LayerdAtomicDesign という考え方
ここから、LayerdAtomicDesign という考え方について具体的な説明を書いていきたいと思います。
以下が LayerdAtomicDesign の基本形になります。
構成の例として使わせて頂いているのは Qiita さんのトップページと記事投稿ページです。
粒度ディレクトリが何箇所かに存在している点が特徴です。
/src
/lib - アプリケーションの依存を一切含まない世界
/atoms
/molecules
/app - アプリケーションの依存を含む世界
/atoms
/molecules
/organisms
/templates
/top-page - トップページの依存を含む世界
/atoms
/molecules
/organisms
/TopPage.tsx
/article-post-page - 記事投稿ページの依存を含む世界
/atoms
/molecules
/organisms
/ArticlePostPage.tsx
依存関係
矢印がつながっている方向への依存が許可されます。
矢印の経路にあるものは直接飛び越えて参照しても問題ありません。
-
lib/molecules
はlib/atoms
を参照可能 -
app/molecules
はapp/atoms
lib/molecules
lib/atoms
を参照可能 -
app/templates/xxx-page/organisms
は上にある全てを参照可能
基本原則
- atoms と molecules にドメイン情報を含めない
- ドメイン情報を含むコンポーネントは全て organisms 以下に収める( XxxPage.tsx はドメイン情報を持って良い )
- templates のコンポーネント以下にも粒度ディレクトリ**を作り、ページ固有のコンポーネントを入れる
ドメイン情報とは
ここで言うドメイン情報とは、top-page
の top
や article
と言ったドメイン用語(それを表す単語)。
または、それらの概念の中で新たに現れる単語のことを言います。
例えば、記事のタイトル title
が含まれるコンポーネントの場合 article
の概念の中で新たに現れる単語を含んでいるので、organisms以下に置く必要があります。
atoms や molecules 内をドメイン用語で文字列検索しても一つも見つからない状態(ディレクトリ名やファイル名含め)であることが求められます。
この構成のメリット
- molecules と organisms の曖昧さを解消できる
- atoms と molecules が再利用性の高いものだけに整理される
- lib app page の3層(3段階)で汎用的なコンポーネントを定義できる
- 各ディレクトリ内のコンポーネント名に統一感が出る
- 各ディレクトリ内のコンポーネントが適切な数に収まる
用語の説明
機能名
anchor button combo-button といった機能の名前を指します。
section や list のように構成に意味がある場合も含めて機能と呼びます。(題名を付ける機能、並べる機能)
依存
2つの物に生じている主従関係のことを指します。
記事タイトルの title
は article
がないと存在できません。
記事がないと記事タイトルは存在できない。
つまり、記事のタイトル title
は article
に依存していると言えます。
概念
何らかの意思を名詞で表したもののことを指します。
英単語 | 単語 | 意味 |
---|---|---|
default | デフォルト | 標準を表す概念 |
primary | 主要 | 物事の中心を表す概念 |
success | 成功 | 目的の達成を表す概念 |
info | 情報 | 知識を伝える概念 |
warning | 警告 | 悪い事を起こさないよう促す概念 |
danger | 危険 | 損害や事故の恐れ表す概念 |
アクション(動作の概念)
いわゆる動詞のことを指します。
アクションは 動作の概念 と捉えることもできます。
英単語 | 単語 | 意味 |
---|---|---|
add | 追加 | 何かを加えるアクション |
create | 作成 | 何かを作り出すアクション |
edit | 編集 | 何かを作り直すアクション |
delete | 削除 | 何かを消し去るアクション |
search | 検索 | 何かを探し出すアクション |
ViewComponent(VC)に関して
VCとは、Mobx、Redux、Flux等の状態管理ライブラリを使わず、アプリケーションの状態を持たないプレゼンテーションコンポーネントのことを指します。
React で言えば、propsを渡されて描画するだけのコンポーネントです。
状態の更新はVCにイベントハンドラを渡して状態管理ライブラリのほうでハンドリングします。
今回はVCの領域のお話をします。
VCの分類
VCには粒度とは別に、階層による分類が存在します。
各階層で使用されるVCの分類について基準となる命名法則も含めて紹介します。
ライブラリVC(LibVC)
アプリケーションにもドメインにも依存を持たないVCです。
汎用的、中立的な立場を維持し、機能や構成を記述します。
- button、combo-button、section のような 機能名
アプリケーションVC(AppVC)
アプリケーションの依存を持つVCです。
アプリケーション内に存在する概念やアクションに対してスタイルや構成を記述します。
- section のような 機能名
- primary-button のような 概念 + 機能名
- search-form のような アクション + 機能名
ドメインVC(DomainVC)
ドメインの依存を持つVCです。
ドメインごとに特化したスタイルや構成を記述します。
- site-header や article-list のような ドメイン名 + 機能名
- article-post-status や article-post-combo-button のような ドメイン名 + アクション + 機能名
テンプレートVC(TemplateVC)
Atomic Design の template にあたるVCです。
各世界の説明
/src/lib - アプリケーション依存を一切含まない世界
いわゆるライブラリに当たるものです。
lib 内は他のリポジトリに分離すると複数のプロジェクトで使えます。
- ライブラリVCのみ入れられる
- organisms と templates は存在しない
- アプリケーションの依存もドメイン情報も含んではいけない
これで、機能名で統一されたディレクトリの並ぶ階層になります。
/src
/lib - アプリケーション依存を一切含まない世界
/atoms - アプリケーションに依存したスタイルや構成を含まないatomのコンポーネント
/anchor
/button
・・・
/molecules - アプリケーションに依存したスタイルや構成を含まないmoleculeのコンポーネント
/combo-button
/list
/section
・・・
/src/app/( atoms | molecules ) - アプリケーションの依存を含む世界
アプリケーション内向けのライブラリです。
- アプリケーションVCのみ入れらる
- 直下は button や icon といった機能名のディレクトリを並べる
- アプリケーション依存は含められるが、ドメイン情報を含んではいけない
これで、機能名 の下に 概念(動作の概念も含む)で統一されたディレクトリの並ぶ階層になります。
/src
/app - アプリケーションの依存を含む世界
/atoms - アプリケーションに依存したスタイルを含むが、ドメイン情報を含まないatomのコンポーネント
/button
/primary-button - アプリケーション内共通のprimaryスタイルを適用
/success-button - アプリケーション内共通のsuccessスタイルを適用
/info-button - アプリケーション内共通のinfoスタイルを適用
/warning-button - アプリケーション内共通のwarningスタイルを適用
/danger-button - アプリケーション内共通のdangerスタイルを適用
Button.tsx - アプリケーション内共通のデフォルトスタイルを適用
・・・
・・・
/icon
/help-icon - アプリケーション内共通のヘルプアイコン
/tag-icon - アプリケーション内共通のタグアイコン
...
/molecules - アプリケーションに依存したスタイルや構成を含むが、ドメイン情報を含まないmoleculeのコンポーネント
/combo-button
/primary-combo-button - アプリケーション内共通のスタイルや構成、primaryの概念を持つデフォルトアイコンやデフォルト文言を含められる
・・・
/form
/search-form - アプリケーション内共通のスタイルや構成、検索の概念を持つデフォルトアイコンやデフォルト文言を含められる
・・・
/section
Section.tsx
/src/app/organisms - アプリケーションのドメイン情報を含む世界
ドメインに依存したものだけが含まれる世界です。
- ドメインVCのみ入れられる
- コンポーネント固有の子コンポーネントは子ディレクトリとして並べる(条件を満たすなら何階層でも)
- atom や molecule の粒度に該当する機能名であっても、ドメイン情報を持つなら organisms に入れる
organism(有機体) を粒度の大きさではなく、ドメイン情報を含むかを基準にすることで、 atom(原子) や molecule(分子) と明確に区別します。
これで、ドメイン名で統一されたディレクトリの並ぶ階層になります。
/src
/app
/organisms - アプリケーションのドメイン情報を含む世界
/article-card - 記事カード
/article-list - 記事リスト
/article-post-status - 記事投稿ステータス
/article-post-combo-button - 記事投稿コンボボタンにアイコンと文言が入った状態のもの
...
/site-header - サイトヘッダー固有でサイトヘッダーでしか使われないドメイン情報を含むコンポーネント達を子に持つ
/community-dropdown-menu
/home-dropdown-menu
/service-dropdown-menu
/user-dropdown-menu
/notification-dropdown-menu
・・・
/site-footer
・・・
・・・
organisms 直下は ドメイン名 のディレクトリでグループ化しても大丈夫です。
/src
/app
/organisms - アプリケーションのドメイン情報を含む世界
/article
/card - 記事カード
/list - 記事リスト
/post-status - 記事投稿ステータス
/post-combo-button - 記事投稿コンボボタンにアイコンと文言が入った状態のもの
...
/site
/header - サイトヘッダー固有でサイトヘッダーでしか使われないドメイン情報を含むコンポーネント達を子に持つ
/community-dropdown-menu
/home-dropdown-menu
/service-dropdown-menu
/user-dropdown-menu
/notification-dropdown-menu
・・・
/footer
・・・
・・・
...
/src/app/templates - アプリケーションのテンプレート
- テンプレートVCのみ入れられる
- テンプレート名のディレクトリの下に粒度ディレクトリを並べる
- 粒度ディレクトリ内の運用方法はテンプレート固有の内容を含められる点を除いて
src/app/
内と同じ
/src
/app
/templates - アプリケーションのテンプレート
/article-post-page - 記事投稿ページ
/atoms - 記事投稿ページ固有のスタイルや構成を含むが、ドメイン情報を含まないatomのコンポーネント
/molecules - 記事投稿ページ固有のスタイルや構成を含むが、ドメイン情報を含まないmoleculeのコンポーネント
/organisms - 記事投稿ページ固有のドメイン情報を含むコンポーネント
/article-editor
/ArticlePostPage.tsx
/top-page - トップページ
/atoms - トップページ固有のスタイルや構成を含むが、ドメイン情報を含まないatomのコンポーネント
/molecules - トップページ固有のスタイルや構成を含むが、ドメイン情報を含まないmoleculeのコンポーネント
/organisms - トップページ固有のドメイン情報を含むコンポーネント
・・・
/TopPage.tsx
・・・
- ライブラリとアプリケーションとページ固有の境界でしっかり分ける
- 1箇所でしか使われないコンポーネントは親コンポーネントの配下に収める
構成のまとめ
上で説明したディレクトリ構成を抽象化するとこのようになります。
よく見ると、縦軸と横軸に統一感があり、キレイに揃っていることがわかるかと思います。
人はこのように四角く収まる形が理解しやすいので、人に優しい構成になっているわけです。
最初に紹介した依存関係の図と併せて見てみるとより理解が深まると思います。
/src
/依存境界 lib
/粒度名 atoms | molecules
/機能名 anchor button | combo-button list section
--------------------------------------------------------- アプリケーションの境界
/依存境界 app
/粒度名 atoms | molecules
/機能名 button icon | combo-button form section
/[概念-][アクション名-]機能名 *-button help-icon tag-icon | primary-combo-button search-form
機能名.tsx
------------------------------------------------------- ドメインの境界
/粒度名 organisms
/ドメイン名-[概念-][アクション名-]機能名 article-* article-post-* | site-*
/[ドメイン名-][概念-][アクション名-]機能名 *-dropdown-menu
or
/ドメイン名 article site
/[概念-][アクション名-]機能名 card list | header footer post-status post-combo-button
/[ドメイン名-][概念-][アクション名-]機能名 *-dropdown-menu
/粒度名 templates
----------------------------------------------------- ページごとの境界
/[ドメイン名-][概念-][アクション名-]機能名 article-post-page | top-page
/app下と同じパターン
補足
名前の型に気を付ける
例えば、LoginForm というコンポーネントがあったとします。
LoginForm の Login はアクション名なので、 src/app/organisms
に置くことができません。
LoginForm が User Member Admin といったドメイン情報を一切含んでいない場合、src/app/molecules
に置くことができます。
その場合、LoginForm はアプリケーションで登場する全ての場所で使える LoginForm であるはずです。
src/app/molecules
でアプリケーション内共通スタイルや共通の構成を記述することになります。
LoginForm が User Member Admin といったドメイン情報を含んでいる場合は、
UserLogin MemberLoginForm AdminLoginForm といった名前にして src/app/organisms
に置くことになります。
コンポーネントの内容とコンポーネント名に不整合があると、正しいコンポーネント管理はできません。
必ずコンポーネントの名前が中身の内容と一致するように合わせ、
それからコンポーネントの名前に併せて粒度ディレクトリへ収めるようにしましょう。
名前の見分け方
ドメイン名とアクション名の見極め方にはコツがあります。
LoginButton
のように目的を持ったボタンにできるようなものはアクション、
UserButton
のように目的を持ったボタンにならない物はドメインか概念です。
PrimaryButton
や DangerButton
が概念に相当します。
それらは主要な場所や危険な場所で使われるボタンということになります。
見極めの難しいものもある
中には名前を文字列として捉えると見極めの難しいものもあります。
例えば Tag です。
記事に依存した Tag であればドメインの中で新たに発生した単語ですが、 このアイコンのように、一般的な Tag の概念を表している場合はドメインの中で新たに発生した単語ではありません。app/atoms/icon/tag-icon
では、一般的な Tag の概念を Icon でどのように表現するかを決定しています。
一般的な Tag の概念は一つであっても、アプリケーションごとに表現のテイストが違うはずなので、個性がでます。
そこをアプリケーション依存を含められる世界の境界にあたる app/atoms
で吸収し、
アプリケーション内部から Tag の一般的な概念を具現化したものを使えるようにしています。
TagIcon 側から見れば
- TagIcon が一般的な Tag の概念を使っているのであれば、
app/atoms/icon
へtag-icon
として置く - TagIcon が記事に依存した概念を使っているのであれば、
app/organisms
へarticle-tag-icon
として置く
記事側から見れば
- 記事が一般的な Tag の概念を使っているのであれば、
app/atoms/icon
のtag-icon
を使う - 記事が記事用の Tag の概念を使っているのであれば、
app/organisms
のarticle-tag-icon
を使う
このように、それぞれが独立した視点で実装したとしても、論理的に整合性が合っていれば自然と実装も繋がるのが、開発において本来あるべき姿です。
単語をただの文字列や識別子として捉えるのではなく、単語自体が持つ文脈や背景、性質などを正確に捉えることで、今まで見えてこなかった微細な差を見極められるようになります。
この考え方は tag-icon
だけでなく、 help-icon
や search-form
にも当てはめることができます。
アプリケーションの境界で概念にスタイルをあて、アクションに目的を達成するための構成を与える。
そのコンポーネントをドメインの境界内で使って構造化する。
すると必然的に統一感のあるアプリケーションが出来上がります。
扱うドメインを変えても、「◯◯を削除する」といった同じシーンであれば同じ印象を与えることができます。
粒度を完全に整理する
app/organisms
にボタンやアイコンも入る可能性があることに違和感が有る方もいらっしゃるかもしれないので補足しておきます。
一応、 lib app の並びに domain を置くことで、ドメインを含む atom や molecule の置き場を作ることはできます。
ただ、アプリケーションのサイズにもよるのですが、大抵の場合この構成は仰々しくなります。
上で紹介した構成の場合、 app/organisms
または app/organisms/ドメイン名
のディレクトリの中に配置されるコンポーネント数は、ある程度スコープが狭められていることから、それほど増えないと考えられるからです。
あるコンポーネント固有の子コンポーネントも、親コンポーネントの配下に配置することから、app/organisms
下のコンポーネントは増えにくくなっています。
増えることが考えられるシーンは、そのスコープに依存した情報が多い場合で、アプリケーション内で統一感のない独特なコンポーネントを作る際などが考えられます。
もし、粒度の違うコンポーネントが同階層にならんで混沌を感じたら、その時が粒度を完全に整理するか検討するタイミングだと思います。
/src
/lib
/atoms
/molecules
/app
/atoms
/molecules
/domain - ドメインを含む世界
/article
/atoms
/status
/post-status
/molecules
/card
/combo-button
/post-combo-button
/list
/organisms
/editor
/templates
/post-page
/site
/atoms
/molecules
/header
/community
/molecules
/dropdown-menu
/home
/molecules
/dropdown-menu
・・・
/footer
まとめ
今回は AtomicDesign でよくハマる問題を解決するべく、
階層の概念を加えた LayeredAtomicDesign という考え方を紹介しました。
この考え方は、ニコニコ生放送の最新実装部分において導入した結果を元に、
記事にするため細部まで整理した物になります。
今までで最もキレイに分類できたので、今後の開発ではもちろん、これまでの実装部分も徐々に LayeredAtomicDesign へ移行していこうと考えています。
ニコニコ生放送では質の高い設計や実装により、開発コストの削減や品質の向上など、様々な取り組みが活発に行われています。
解りやすくキレイに分類されたコンポーネントの世界で一緒に開発したい!という仲間も大歓迎です。
最後に
今回紹介した内容は、新たにルールを作ったというものではありません。
すでに物事の関係性として存在していた情報を一つ一つ正確に読み取り直しただけなのです。
新しいルールを作るというのは、新たな主観を生み出すことであり、覚えることや守ることを増やします。
それに対して、すでに存在している物事の関係性を見抜くというのは、関心の捉え方を改めるという学びです。
対象は何も変わっていないのに、考え方を改めたら物事が理解できることがあるように、
脳に保存されている状態(context)を更新することにより、対象の理解を阻害する先入観を無くし、
関数的に答えを得ることができるようになるのです。
今回紹介した記事では、細部までどのようにすると整理されるのか、ということに触れました。
これに対して「ガチガチに決めるのは良くない」という意見もあるかもしれません。
しかし、その言葉が表していることは「ガチガチのルールを作ったら、覚えることや守ることが増えるので負担になる」という意味であり、「すでに存在している物事の関係性を細部まで見抜き、それに委ねる」こととは全く別のことであると考えています。
新しい世界を作ったのではなく、すでに存在していた暗闇の世界に光を照らしたというほうがしっくりきます。
フロントエンドの世界に状態と写像の考え方が現れたとき、世界は大きく変わりました。
しかし、それも新しい世界を作ったわけではなく、ずっと存在はしていた(やればできた)けど気付いていなかっただけなのです。
元々存在している物事の関係性を利用して、主観や考えることを減らし、複雑度に対抗する。
年々増していくアプリケーションの複雑度への対抗手段として、とても有効であると感じています。