共通部分をスマートに管理するディレクトリ構成
はじめに
ニコニコ生放送でフロントエンドを担当している misuken です。
今回は共通部分をスマートに管理するディレクトリ構成のノウハウを紹介します。
日頃の開発において、抽象と具象の関係や基底と派生の関係で、共通部分を切り出したいという場面がよくあるかと思います。
いざ切り出してみると、ディレクトリ構成がしっくり来なかったり、依存関係が複雑になったり、悩みを抱えたまま開発を進めている方も多いのではないでしょうか?
しかし、なぜうまくいかないのか、どうすればうまくいくのか、理由を知ってしまえば迷うことはありません。
大切なポイントは3つ。
- 抽象と具象や基底と派生のようなものは、親子ではなく兄弟の位置関係に配置する
- 抽象のディレクトリ名は
_abstract
が適している (基底としてそれも使用できるなら_base
がおすすめです) - 抽象側(基底)を上に具象側(派生)は下に並ぶようにすると人間は理解しやすい
安定した形を手に入れ、スマートなディレクトリ構成で開発を進めましょう。
前提
- Reactでコンポーネントを管理する例で説明します
- 当然React以外の様々なディレクトリ構成でも応用できます
- 今回は抽象と具象の例で説明しますが、基底と派生の関係でも適用できます
- BCD Design の概念も覚えておくとより体系的に理解できます
- 図に出てくる4色の矢印の方向は依存を表します
- 緑: 良好(コンポーネントの依存)
- 青: 良好(exportの依存)
- 黄: 注意(参照方向がわかりにくくなるもの)
- 赤: 警告(複雑さを増すもの)
- 図に出てくる青い枠線はコンポーネントやパッケージの単位を表します
よくあるパターン
入力欄の抽象コンポーネント Field
を例にします。
これはフィールドの抽象コンポーネントで、実際には CheckBox
TextBox
SelectBox
など具体的な入力UIと連携して使う想定のコンポーネントとします。
具体的な入力UI以外は共通でよく、共通部分にはラベルや、エラーなどのメッセージを表示するリストも含まれます。(説明のために敢えて子コンポーネントとして持たせています)
この抽象的なコンポーネントと具象コンポーネントを収めるディレクトリ構成について考えてみます。
1. フラットに並べる
まず最も単純なフラットに並べる方法です。
フラットに並べるのは単純ですが、アルファベット順に並んだとき、各フィールドは他のコンポーネントと混ざり、広い範囲に分散してしまいます。
また、同階層には同様のコンポーネントが他にもあるはずで、参照方向が上へ下(黄色線)へと飛び交い、依存関係が複雑に感じられるようになります。
この構成は雑多に並んでいる印象を受けやすく、コンポーネントの増加と共に見通しが悪くなっていきます。抽象と具象の関係が埋もれたり、どれだけ派生パターンがあるのかわかりにくく、全体像を把握しにくい構成と言えます。
2. ツリー上の親子関係にする
次にツリー上の親子関係にする方法です。
ツリー上の親子関係にすると、フィールドの部品(LabelやMessageList)と具象のフィールドのように軸の違うものが一つの軸に並ぶため、まとめたようでごちゃごちゃした印象になります。
参照方向もディレクトリ階層を行き来(赤線が子から親を参照)することになるので、依存関係も複雑に感じられるようになります。
この構成は、参照方向に秩序が感じられないため、おかしな参照が紛れ込んでも気付きにくくなります。割れ窓理論のように悪循環の入り口になりやすく、危険な香りのする構成と言えます。
3. 兄弟関係にする
最後は兄弟関係にする方法です。
兄弟関係とは、抽象も具象も1つの軸に存在していると捉え、兄弟として並べることを指します。
抽象(_abstract
)の中はフィールドの部品(Label
や MessageList
)など、どのような構成であるかがわかりやすく、Field
直下はどのような具象が存在するかや抽象具象の関係が明瞭です。
依存関係も参照が上下左右の上方向に統一され、単方向依存であることがはっきりとわかります。
エディタが "ディレクトリが先でファイルが後" という並び順であれば、それも相まってコンポーネント同士の参照は全て上方向に揃います。
こうなっていれば _abstract
の中に構成要素が増えたとしても、 Field
内に新たな具象が増えたとしても、複雑さが増すことはありません。(スケールする軸が確保された状態)
この構成は、参照方向に秩序があり、各責務の境界もはっきりしていて、全体の把握が容易です。
抽象と具象に分ける利点を最大限に得られ、複雑さやスケールに対抗できる構成と言えます。
抽象部分の定義のexportに関して
Field
として定数などの定義を export
したいが、Field.tsx
は抽象コンポーネントなので export
はしたくないといったシーンもあります。
このような場合は、定義を Field/_abstract/_definition.ts
に記述し Field/_index.ts
から直接 export
することで実現できます。(あくまで手軽さを優先する場合のやり方ではあります)
Field
直下の index.ts
はこのように書きます。
export * from "./_abstract/_definition";
// export * as Field from "./_abstract/_definition"; 定義に名前空間を付けたければこのように書きます
export * from "./CheckBoxField";
export * from "./SelectBoxField";
export * from "./TextBoxField";
兄弟関係にする考え方のポイント
兄弟関係にする考え方のポイントとして、ディレクトリには型があると考えると理解しやすくなります。
ディレクトリの型を意識すると Field
ディレクトリはこう見えてきます。
-
ListDirectory<T extends ComponentDirectory>
型である - 内部に複数の
ComponentDirectory
型を持つ -
ListDirectory
型はindex.ts
で各コンポーネントをexport
する責務のみを持つ
そして各コンポーネントのディレクトリはこう捉えられます。
-
ComponentDirectory
型である - コンポーネントの構成ファイル(
*.css
*.tsx
*.stories.tsx
)や子のComponentDirectory
を持つ - コンポーネントがどのように構成されているかの責務のみを持つ
ディレクトリ階層ごとに目的に合った型を適用していると考えれば、抽象的なコンポーネントも具象コンポーネントと同一視できるため、 ComponentDirectory
型として揃え、同じ型をリスト系の型の要素にするというのは自然な流れです。
これらを踏まえてみると 1. フラットに並べる
と 2. ツリー上の親子関係にする
の問題は以下であったことがわかります。
- 広いスコープに散らばり、抽象具象の役割がはっきり見えないこと
- 2つの型がマージされて型が歪であること
variantのような実装を加える場合
Field
が variant="primary"
のような種類を持つことになったとします。Field.tsx
内で解決しても良さそうですが、ファイル内の行数が増えて複雑化するので分離して管理したい場面を考えてみます。
variant
を導入する場合、各入力UIはそれぞれの責務で variant
を所有しているでしょうから、Field
の責務で解決するのは入力UI依存を除いた部分ということになります。Field
の入力UI依存を除いた部分は _abstract
内にまとまっているため、_abstract
内に閉じられることがわかります。
variant
も複数パターンを有する構成自体は Field
と同様で、扱う内容が違うだけであるため ListDirectory<T extends VariantDirectory>
のようなディレクトリ型であると捉えられます。
この構成なら、Field.tsx
内で variant="primary"
などと渡されたら、その見た目が反映されるように _variants/Primary
の実装を利用するだけで完結します。具体的な実装は _variants/Primary
の中にあるため、Field.tsx
内の行数が増えることもなく、variant
の種類が増えたとしても適切にスケールできます。
また、各 variant
の共通定義は _variant/_abstract
へ書けるため、コンポーネントのときと同じ構成で理解が容易です。
別パッケージに切り出しても変わらない兄弟関係
次に兄弟関係の強みをもう一つ紹介しましょう。
もしも抽象コンポーネントを別パッケージに切り出すことになったとします。
それを2つのパッケージで具体的なコンポーネントを実装する形にしたときも、抽象部分は垂直方向に移動するだけで兄弟関係の位置は変わりません。(library/Field
内が単一になるので _abstract
の階層は無くしています)
このように、兄弟関係の位置にあると疎結合且つ凝集度が高く、責務の境界がはっきりするため、構成の変化にも柔軟に対応できることがわかります。
アンダースコアを含むディレクトリ名を使いたくない
アンダースコアを含むディレクトリ名を避けると、スコープ管理のために階層が増えるため以下のような形になります。
仰々しくなるのでオススメはしませんが、意味合いの理解には役立つかもしれません。
アンダースコアを使うことのメリットは以下の通りです。
- 階層を簡略化できる
- 抽象や基底の存在がひと目でわかる
- 参照方向を統一できる
並び順を制御するためにアンダースコアを乱用するべきではありませんが、特定の目的のために規格を作り、その範囲内でのみ使用する分には問題ありません。
注意点
関心を含めてはいけない
これまでの説明を読んで「フィールド系は全て Field
の中にあれば良く、そうすれば便利だ」と考えるのは誤りです。
例えば、 CategorySelectBoxField
や SearchTextBoxField
というコンポーネントがある場合、これらは Field
に含めるべきではありません。
これは BCD Design を理解しているとわかりやすいのですが、そもそもそれらが存在する単語の層が違うので、Field
側にまとめてしまうと関心の分散に繋がってしまいます。
Category
系はカテゴリでまとまり、 Search
系は検索でまとまっていたほうが凝集度の観点から望ましいのです。
このあたりの話は以下の記事で詳しく説明しています。
TextBoxField を TextBox 側にまとめてはいけない
TextBoxField
を Field
側にまとめましたが、 TextBox
側にまとめてはいけないのか?と思う方もいるかもしれないので補足しておきます。
UIのレベルではそのUIの型が一致するかでまとめるときれいにまとまります。
-
TextBoxField
はField
型である -
TextBoxField
はTextBox
型ではない
これは BCD Design の Base にあたる部分では型(後方一致のUI名)でまとめ、Case Common Domain では前方一致の状況名や関心名でまとめることを意味します。
なぜ物によってまとめ方が変わるかというと、UIの提供側と利用側の違いがあるためです。
Base はUIを提供する側のカタログですが、Case Common Domain は特定の状況や関心における具体的な UI の利用方法を実装する場所です。前者はUIの型としての凝集度、後者は特定の状況や関心の凝集度、それぞれが高いほど探しやすさや保守性が高くなります。
まとめかたの軸を正しく選択できるかが非常に重要になるので、ここの感覚はしっかり抑えておくと良いでしょう。
まとめ
今回は共通部分をスマートに管理するディレクトリ構成のノウハウを紹介しました。
何となく似たような結果になっている場合もあると思いますが、構成と依存関係を可視化したり理論を理解することで、ディレクトリ構成の精度を上げる手助けになれば幸いです。
また、今回紹介したディレクトリ構成は細部にあたりますが、大きな部分を分類できる BCD Design と併用すればプロジェクトの大部分に秩序をもたらすことができるでしょう。BCD Design も参照方向が上に揃うよう BCD から始まるディレクトリで構成できるように考えられたものなので、全体の参照方向がきれいに整います。
おまけ
ディレクトリ階層ごとに目的に合った型を適用していると考えれば、抽象的なコンポーネントも具象コンポーネントと同一視できるため
上記の "同一視" の部分は、絵柄が違うものの形や情報の構成が同じカードを束ねるようなイメージに基づくものです。
以前書いた スケールの大きな物を設計する際の考え方をトランプのカードで説明する の発想もヒントになるかもしれません。
株式会社ドワンゴでは、様々なサービス、コンテンツを一緒につくるメンバーを募集しています。 ドワンゴに興味がある。または応募しようか迷っている方がいれば、気軽に応募してみてください。
Discussion