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
index
Component
- Directory
ComponentDirectory
VariantDirectory
ListDirectory<T extends ComponentDirectory | VariantDirectory>
index
その階層のディレクトリやファイルをexportする役割のファイルです。以降特筆しませんが、すべてのDirectory型で存在することができます。
Component
コンポーネントを実装するファイルです。tsx
だけではなくhooks
やstyle
、story
などもComponent
型に含まれます。
ComponentDirectory
Component
型のファイルを内包するディレクトリです。_abstract
や_base
といったディレクトリもComponentDirectory
型に含まれます。また、Component
型以外にも以下の型を内包することができます。
ComponentDirectory
ListDirectory<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