BCD Designにおける共通部分をスマートに管理するディレクトリ構成から学んだこと
BCD Designとは、以下の記事で提唱されている、フロントエンドにおけるコンポーネントの分類手法です。
BCD Designでは、コンポーネントを「Base, Case, Domain」の3つの概念と「atoms, molecules」の2つの粒度の軸で体系的に分類します。
その結果、煩雑になりがちなコンポーネントの分類において、非常に高いスケーラビリティを保つことができます。
さて、BCD Designでは上記の2つの軸によって分類されたディレクトリ構造だけではなく、以下の記事で紹介されている「共通部分をスマートに管理するディレクトリ構成」を組み込むことができます。
今回はこの「共通部分をスマートに管理するディレクトリ構成」に焦点を当て、頭の中で整理したことについてまとめたいと思います。
概要
まずはじめに、「共通部分をスマートに管理するディレクトリ構成」の概要について説明します。
しかし、本家記事様を読んだ方が理解しやすい部分も多いと思われるので、ここでは基本となるディレクトリ構造の説明のみを行います。
抽象コンポーネントと具象コンポーネント
src/
└── components/
└── base/
└── molecules/
└── Field/
├── _abstract/
│ ├── Label/
│ ├── MessageList/
│ ├── Field.hooks.tsx
│ ├── Field.module.css
│ ├── Field.stories.tsx
│ ├── Field.tsx
│ └── index.ts
├── CheckBoxField/
│ ├── CheckBoxField.module.css
│ ├── CheckBoxField.stories.tsx
│ ├── CheckBoxField.tsx
│ └── index.ts
├── SelectBoxField/
├── TextBoxField/
└── index.ts
本家記事と同様にFieldコンポーネントを例に説明します。
まず、Fieldコンポーネントとはフォームなどで用いられる入力欄のUIです。ラベルやバリデーションエラーなどのメッセージリスト、入力部分のUIがセットになっています。
ここで、入力部分のUIについて考えると、TextBoxやCheckBox、SelectBoxなど様々な種類があるとわかります。
しかし、入力部分のUI以外のラベル部分やメッセージリスト部分はどのFieldでも共通です。このことから、TextBoxFieldやCheckBoxFieldはそれぞれ独立したコンポーネントではなく、Fieldという抽象コンポーネントの具象コンポーネントであることがわかります。
よって、これらの具象コンポーネントを抽象コンポーネントのディレクトリでまとめることができます。
そして、せっかくまとめたわけですから、ラベルやメッセージリストといった共通の部品はそれぞれの具象コンポーネントで使い回せるように共通化したいと考えます。
しかし、具象コンポーネントのディレクトリと同階層に共通の部品をおいてしまうと具象コンポーネントで揃っていたディレクトリの粒度がずれてしまいます。
そこで、_abstractディレクトリが有効となります。共通の部品は_abstractディレクトリにいれることで抽象(共通の部品)を具象と兄弟要素として扱えるようになります。これによって、ディレクトリの粒度をくずさないですむようになりました。
_baseディレクトリ
_abstractディレクトリの他に_baseディレクトリを使用することができます。_baseディレクトリは、基底として_baseディレクトリ自体もコンポーネントとして使用可能な状態となっている場合に使われます。
親ディレクトリからexportされるなら_base、されないなら_abstractと覚えておくと簡単かもしれません。
variant
次に、Fieldがprimaryやsecondaryといったvariantを持つ場合を考えます。
TextBoxやCheckBoxといった入力部分のUIのvariantに関しては各具象コンポーネントの責務となるため、Fieldが持つvariantは_abstract内に閉じられることがわかります。
src/
└── components/
└── base/
└── molecules/
└── Field/
├── _abstract/
│ ├── _variant/ # ここにvariantを定義する
│ │ ├── _abstract/
│ │ │ ├── Field.tsx
│ │ │ └── index.ts
│ │ ├── Primary/
│ │ ├── Secondary/
│ │ └── index.ts
│ ├── Label/
│ ├── MessageList/
│ ├── Field.hooks.tsx
│ ├── Field.module.css
│ ├── Field.stories.tsx
│ ├── Field.tsx
│ └── index.ts
├── CheckBoxField/
│ ├── CheckBoxField.module.css
│ ├── CheckBoxField.stories.tsx
│ ├── CheckBoxField.tsx
│ └── index.ts
├── SelectBoxField/
├── TextBoxField/
└── index.ts
variantは_variantというディレクトリにまとめられます。
各variantの詳細な実装はPrimary/やSecondary/といったバリアントディレクトリの中で定義されます。また、各variantの共通の部分は_variantの中の_abstractにおかれます。
では、Field/_abstract/Field.tsxとField/_abstract/_variant/_abstract/Field.tsxの違いはなんでしょうか?
それは、責務の違いです。基本的なマークアップが行われるのはField/_abstract/_variant/_abstract/Field.tsxですが、Field/_abstract/Field.tsxではpropsでわたされたvariantの値を見てPrimaryかSecondaryを出し分けることしか行いません。
つまり、最もプリミティブなコンポーネントになるのは最下層にあるコンポーネントということです。
このルールを守ることで、依存関係がとてもスッキリします。
まず、TextBoxFieldやCheckBoxFieldといった具象コンポーネントは兄弟要素の抽象コンポーネントであるField/_abstractにのみ依存します。
次に、variantが存在する場合、Field/_abstractは兄弟要素であるLabelやMessageList、_variantディレクトリにのみ依存します。
そして、Field/_abstract/_variantは兄弟要素のField/_abstract/_variant/_abstractにのみ依存します。
このように、「共通部分をスマートに管理するディレクトリ構成」では上方向の兄弟要素にのみ依存することとなります。これによって、理解しやすいコンポーネント設計を行うことができます。
ディレクトリの型
「共通部分をスマートに管理するディレクトリ構成」では、各ディレクトリやファイルに型を当てはめることができます。
型は以下の5種類です。
- File
indexComponent
- Directory
ComponentDirectoryVariantDirectoryListDirectory<T extends ComponentDirectory | VariantDirectory>
index
その階層のディレクトリやファイルをexportする役割のファイルです。以降特筆しませんが、すべてのDirectory型で存在することができます。
Component
コンポーネントを実装するファイルです。tsxだけではなくhooksやstyle、storyなどもComponent型に含まれます。
ComponentDirectory
Component型のファイルを内包するディレクトリです。_abstractや_baseといったディレクトリもComponentDirectory型に含まれます。また、Component型以外にも以下の型を内包することができます。
ComponentDirectoryListDirectory<ComponentDirectory>ListDirectory<VariantDirectory>
VariantDirectory
primaryやsecondaryといったコンポーネントのバリエーションを内包するディレクトリです。VariantDirectory型には以下の特徴があります。
-
VariantDirectory型はListDirectory<VariantDirectory>型の直下にしか存在できない -
VariantDirectory型はComponent型のみ内包することができる
ListDirectory<T extends ComponentDirectory | VariantDirectory>
T型のディレクトリを内包するディレクトリです。T型以外の型は内包できないという特徴を持っています。
ListDirectory<VariantDirectory>
VariantDirectory型のディレクトリを内包するディレクトリです。_variantという名前のディレクトリになります。なお、先述のとおりVariantDirectory型以外の型は内包できませんが、例外として_abstractや_baseディレクトリは内包することが可能です。
ここまで5種類の型について説明をしてきました。
そこで、先程のディレクトリ構造と型を照らし合わせたいと思います。
src/
└── components/
└── base/
└── molecules/
└── Field/ -> ListDirectory<ComponentDirectory>
├── _abstract/ -> ComponentDirectory
│ ├── _variant/ -> ListDirectory<VariantDirectory>
│ │ ├── _abstract/ -> ComponentDirectory
│ │ │ ├── Field.tsx -> Component
│ │ │ └── index.ts -> index
│ │ ├── Primary/ -> VariantDirectory
│ │ ├── Secondary/ -> VariantDirectory
│ │ └── index.ts -> index
│ ├── Label/ -> ComponentDirectory | ListDirectory<ComponentDirectory>
│ ├── MessageList/ -> ComponentDirectory | ListDirectory<ComponentDirectory>
│ ├── Field.hooks.tsx -> Component
│ ├── Field.module.css -> Component
│ ├── Field.stories.tsx -> Component
│ ├── Field.tsx -> Component
│ └── index.ts -> index
├── CheckBoxField/ -> ComponentDirectory
│ ├── CheckBoxField.module.css -> Component
│ ├── CheckBoxField.stories.tsx -> Component
│ ├── CheckBoxField.tsx -> Component
│ └── index.ts -> index
├── SelectBoxField/ -> ComponentDirectory
├── TextBoxField/ -> ComponentDirectory
└── index.ts -> index
このように整理してみるとわかることがあるのですが、ComponentDirectory型の直下にあるComponentDirectory型は、内部を確認しないとListDirectory<ComponentDirectory>型との区別ができません。
しかし、このような汎用性があることでディレクトリ構造を再帰的に適用することができ、スケーラビリティの高さにつながっていると考えられます。
依存関係と禁則
ListDirectoryとComponentDirectoryを区別することで依存関係は兄弟ディレクトリか兄弟要素のみで完結するようになります。
兄弟ディレクトリとは、同じ親ディレクトリを持つディレクトリ同士の関係を指します。
例えば、CheckBoxField/ChexBoxField.tsxは兄弟ディレクトリである_abstractにのみ依存しています(厳密に言えば_abstract/index.ts)。
そして、兄弟要素というのは、同階層にあるディレクトリやファイルの関係です。
例えば、_abstract/Field.tsxは_abstract/_variant, _abstract/Label, _abstract/MessageList, _abstract/Field.hooks.tsx, ...と兄弟要素にのみ依存しています。
_abstractや_baseディレクトリを置くことの利点は、子が親を参照することがなくなる点です。
例えば、以下のようなディレクトリ構造の場合、CheckBoxField.tsxは兄弟ディレクトリではなく親ディレクトリの要素であるField.tsxに依存することとなります。
src/
└── components/
└── base/
└── molecules/
└── Field/
├── Label/
├── MessageList/
├── Field.tsx
├── CheckBoxField/
│ ├── CheckBoxField.tsx
│ └── index.ts
├── SelectBoxField/
├── TextBoxField/
└── index.ts
これを踏まえてimportとexportのルールを整理してみます。すると、以下のような禁則があるとわかります。
- 子要素が親要素に依存すること
-
index以外から直接importすること - 兄弟要素以外をexportすること
variantの応用
今までの例ではvariantは1つの軸(色)のみでしたが、2つ以上の軸(例えば、色・形)があった場合のディレクトリ構造はどうなるでしょうか?
ここでは2種類の方向性を提示してみます。
再帰的手法
src/
└── components/
└── base/
└── atoms/
└── Button/
├── _base/
│ ├── _variant/
│ │ ├── _base/
│ │ │ ├── _variant/
│ │ │ │ ├── _base/
│ │ │ │ │ ├── Button.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── Rounded/
│ │ │ │ ├── Rect/
│ │ │ │ └── index.ts
│ │ │ ├── Button.tsx
│ │ │ └── index.ts
│ │ ├── Primary/
│ │ ├── Secondary/
│ │ └── index.ts
│ ├── Button.tsx
│ └── index.ts
├── ClearButton/
├── SearchButton/
└── index.ts
このように、_variantの中の_abstractや_baseの中に、さらに_variantを追加する手法です。
この手法の利点は、Button/_base/_variant/_base/Button.tsxはRoundedまたはRectのどちらなのかだけを考慮すればよく、同様にButton/_base/Button.tsxはPrimaryまたはSecondaryのいずれかを選択するだけで済むという点です。
しかし、この手法には欠点が2つあります。
まず、最もプリミティブなButtonが、variantが追加されるたびに深い階層へ押し込まれていくという点です。
上記の例だと、RoundedとRectが追加されるまでは最もプリミティブなButtonはButton/_base/_variant/_base/Button.tsxでした。
そこにRoundedとRectが追加されたことで、最もプリミティブなButtonはButton/_base/_variant/_base/_variant/_base/Button.tsxへと変化しました。
このように、variantの軸が増えるたびに階層が深くなっていきすべてのButton.tsxに修正が必要なため可読性と保守性が犠牲になります。
次に、排他的なvariantが作れなくなるという点です。
現在の構成では、PrimaryとSecondaryのスタイルと、RoundedとRectの形状を自由に組み合わせることが可能ですが、例えばPrimaryとSecondaryに加えてGhostを追加した場合、Ghostはボタンの形状を定義しないため、RoundedやRectを適用できません。
このようなケースが発生すると、現在の手法では適切に管理できなくなります。
兄弟ディレクトリにしてexportで制御
src/
└── components/
└── base/
└── atoms/
└── Button/
├── _base/
│ ├── _variant/
│ │ ├── _abstract/
│ │ │ ├── Button.tsx
│ │ │ └── index.ts
│ │ ├── Primary/
│ │ ├── Secondary/
│ │ ├── Rounded/
│ │ ├── Rect/
│ │ ├── Ghost/
│ │ └── index.ts
│ ├── Button.tsx
│ └── index.ts
├── ClearButton/
├── SearchButton/
└── index.ts
複数の軸があってもすべて同じ_variantで管理する手法です。
ただし、依存関係で軸を制御します。
上記の例だと、Primary/とSecondaryは_abstractに依存しています。
また、Rounded/とRect/はPrimary/とSecondary/に依存しており、Ghost/は_baseに依存しています。
そして、index.tsがexportするのはRounded/とRect/、Ghost/のみとなります。
こうすることで、階層を深くしないまま2軸以上のvariantを扱うことができるようになります。
ただし、index.tsがexportする対象を正確に把握していないといけないという欠点があります。
exportする対象がわかりやすいようにexportしないディレクトリを_Primaryや_Secondaryといったようにアンダーバーをつけてもいいかもしれません。
この手法では、複数の軸のvariantを同階層の_variantで管理します。
ただし、依存関係を利用して軸を制御します。
上記の例では、PrimaryとSecondaryは_abstractに依存しています。
また、RoundedとRectはPrimaryとSecondaryに、Ghostは_baseにそれぞれ依存しています。
そのため、index.tsがexportするのはRounded、Rect、Ghostのみとなります。
こうすることで、ディレクトリ階層を深くせずに、複数の軸を持つvariantを扱えるようになります。
ただし、index.tsがexportする対象を正確に把握する必要があるという欠点があります。
exportの対象を明確にするため、exportしないディレクトリには_Primaryや_Secondaryのようにアンダーバーを付けるのも一案です。
おわりに
今回は、「共通部分をスマートに管理するディレクトリ構成」を実際に運用するにあたって重要だと思う点について考察してみました。
通常のBCD Designにおいてひとつのコンポーネントをパーツ分けして管理したいという場面はよくあったので、このディレクトリ構造を用いて管理してみたいと思います。
Discussion