Zenn
🏗

BCD Designにおける共通部分をスマートに管理するディレクトリ構成から学んだこと

2025/03/10に公開

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について考えると、TextBoxCheckBoxSelectBoxなど様々な種類があるとわかります。

しかし、入力部分のUI以外のラベル部分やメッセージリスト部分はどのFieldでも共通です。このことから、TextBoxFieldCheckBoxFieldはそれぞれ独立したコンポーネントではなく、Fieldという抽象コンポーネントの具象コンポーネントであることがわかります。

よって、これらの具象コンポーネントを抽象コンポーネントのディレクトリでまとめることができます。
そして、せっかくまとめたわけですから、ラベルやメッセージリストといった共通の部品はそれぞれの具象コンポーネントで使い回せるように共通化したいと考えます。

しかし、具象コンポーネントのディレクトリと同階層に共通の部品をおいてしまうと具象コンポーネントで揃っていたディレクトリの粒度がずれてしまいます。

そこで、_abstractディレクトリが有効となります。共通の部品は_abstractディレクトリにいれることで抽象(共通の部品)を具象と兄弟要素として扱えるようになります。これによって、ディレクトリの粒度をくずさないですむようになりました。

_baseディレクトリ

_abstractディレクトリの他に_baseディレクトリを使用することができます。_baseディレクトリは、基底として_baseディレクトリ自体もコンポーネントとして使用可能な状態となっている場合に使われます。

親ディレクトリからexportされるなら_base、されないなら_abstractと覚えておくと簡単かもしれません。

variant

次に、Fieldprimarysecondaryといったvariantを持つ場合を考えます。

TextBoxCheckBoxといった入力部分の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.tsxField/_abstract/_variant/_abstract/Field.tsxの違いはなんでしょうか?

それは、責務の違いです。基本的なマークアップが行われるのはField/_abstract/_variant/_abstract/Field.tsxですが、Field/_abstract/Field.tsxではpropsでわたされたvariantの値を見てPrimarySecondaryを出し分けることしか行いません。

つまり、最もプリミティブなコンポーネントになるのは最下層にあるコンポーネントということです。

このルールを守ることで、依存関係がとてもスッキリします。

まず、TextBoxFieldCheckBoxFieldといった具象コンポーネントは兄弟要素の抽象コンポーネントであるField/_abstractにのみ依存します。

次に、variantが存在する場合、Field/_abstractは兄弟要素であるLabelMessageList_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だけではなくhooksstylestoryなどもComponent型に含まれます。

ComponentDirectory

Component型のファイルを内包するディレクトリです。_abstract_baseといったディレクトリもComponentDirectory型に含まれます。また、Component型以外にも以下の型を内包することができます。

  • ComponentDirectory
  • ListDirectory<ComponentDirectory>
  • ListDirectory<VariantDirectory>

VariantDirectory

primarysecondaryといったコンポーネントのバリエーションを内包するディレクトリです。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>型との区別ができません。

しかし、このような汎用性があることでディレクトリ構造を再帰的に適用することができ、スケーラビリティの高さにつながっていると考えられます。

依存関係と禁則

ListDirectoryComponentDirectoryを区別することで依存関係は兄弟ディレクトリか兄弟要素のみで完結するようになります。

兄弟ディレクトリとは、同じ親ディレクトリを持つディレクトリ同士の関係を指します。
例えば、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.tsxRoundedまたはRectのどちらなのかだけを考慮すればよく、同様にButton/_base/Button.tsxPrimaryまたはSecondaryのいずれかを選択するだけで済むという点です。

しかし、この手法には欠点が2つあります。

まず、最もプリミティブなButtonが、variantが追加されるたびに深い階層へ押し込まれていくという点です。
上記の例だと、RoundedRectが追加されるまでは最もプリミティブなButtonButton/_base/_variant/_base/Button.tsxでした。
そこにRoundedRectが追加されたことで、最もプリミティブなButtonButton/_base/_variant/_base/_variant/_base/Button.tsxへと変化しました。

このように、variantの軸が増えるたびに階層が深くなっていきすべてのButton.tsxに修正が必要なため可読性と保守性が犠牲になります。

次に、排他的なvariantが作れなくなるという点です。
現在の構成では、PrimarySecondaryのスタイルと、RoundedRectの形状を自由に組み合わせることが可能ですが、例えばPrimarySecondaryに加えてGhostを追加した場合、Ghostはボタンの形状を定義しないため、RoundedRectを適用できません。
このようなケースが発生すると、現在の手法では適切に管理できなくなります。

兄弟ディレクトリにして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で管理します。
ただし、依存関係を利用して軸を制御します。

上記の例では、PrimarySecondary_abstractに依存しています。
また、RoundedRectPrimarySecondaryに、Ghost_baseにそれぞれ依存しています。

そのため、index.tsがexportするのはRoundedRectGhostのみとなります。

こうすることで、ディレクトリ階層を深くせずに、複数の軸を持つvariantを扱えるようになります。

ただし、index.tsがexportする対象を正確に把握する必要があるという欠点があります。
exportの対象を明確にするため、exportしないディレクトリには_Primary_Secondaryのようにアンダーバーを付けるのも一案です。

おわりに

今回は、「共通部分をスマートに管理するディレクトリ構成」を実際に運用するにあたって重要だと思う点について考察してみました。
通常のBCD Designにおいてひとつのコンポーネントをパーツ分けして管理したいという場面はよくあったので、このディレクトリ構造を用いて管理してみたいと思います。

GitHubで編集を提案

Discussion

ログインするとコメントできます