🧩

AtomicDesign 境界線のひき方

2020/09/21に公開

AtomicDesign はコンポーネント粒度の指標として共有し易く、多くのプロジェクトで採用されています。しかしながら、その設計概念が先立ってしまい、いくらかの課題を抱えている場合があります。

  • 1.影響範囲が特定しにくい
  • 2.依存関係が特定しにくい
  • 3.汎用性が低くなりがち
  • 4.汎用性の高低が判別できない

多くの場合「粒度」だけを基準にしてしまい、横断的コンテキストに「早期区分」 してしまっていることが直接的原因です。

「関心範囲の広さ」区分

アプリケーションを構成するモジュールは「関心範囲の広さ」で区分を行うことが鉄則です。次の2つのhelper.tsを見てください。全く同じ関数が定義されていたとしても、どこで利用される想定のものなのか、すぐに判別できますね。utilsは、プロジェクト内で横断的に再利用される可能性が高い(関心範囲が広い)ものが集約されます。

utils/helper.ts // 関心範囲が広い(横断的)
some/usecase/helper.ts // 関心範囲が狭い(限定的)

AtomicDesign 採用プロジェクトにおいて、components/molecules の様に「同粒度の Component は同一空間に集約しているケース」をよく見かけます。ここに格納するということは「横断的コンポーネント」として参照されることに他なりません。

横断的コンポーネント

横断的コンポーネントと、冒頭課題の関連を見ていきます。これらの課題が積み重なると「リファクタリングし辛い」「作り込みコストがかかる」という、悪影響が現れてきます。

1.影響範囲が特定しにくい

横断的コンテキストに属するため、ひとつの修正が複数箇所に影響を及ぼします。影響範囲が特定しにくいため、リファクタリングにともなう精神的ハードルが高くなります。

2.依存関係が特定しにくい

例えばFormButtonという atom があった場合。どこの molecules・organisms から利用されているのか、列挙するのに一苦労です。この様な影響範囲を限定的にするためContactFormButtonの様な、特定のユースケースに向けた「接頭辞」を持つコンポーネントが多く作られることになります。

3.汎用性が低くなりがち

ContactFormButtonの影響範囲は限定的ですが、汎用性は低いです。この様なコンポーネントが大量に生まれ、components/atoms空間を埋めつくすことになります。接頭辞で溢れかえり、可読性は下がる一方です。

4.汎用性の高低が判別できない

components/atoms空間のなかから、汎用性の高い・低いものはどれでしょうか?大量にあるコンポーネントの中から、目的のものを探しだしたり、変更を加える労力は少なくないでしょう。途中で不要になった、使われていないコンポーネントも残っているかもしれません。

限定的コンポーネント

components/templates/Articles/Card.tsx の様に、特定のコンポーネントディレクトリに隠蔽されたコンポーネントが「限定的コンポーネント」です。Articlesの文脈からみれば、 Card.tsx でしかなく、親子関係・ユースケースも明瞭なものです。まったく同じ実装であっても、属するディレクトリを変更するだけで、憂慮は大幅に削減されます。

【限定的コンポーネント】

components/templates/Articles/List
components/templates/Articles/List/Card
components/templates/Articles/Button

【横断的コンポーネント】

components/organisms/ArticlesList
components/molecules/ArticlesListCard
components/atoms/ArticlesButton

限定的コンポーネントを横断的なものに移行する

コンポーネントは、はじめは限定的コンテキストで実装するべきでしょう。共通利用される頃合いに、リファクタリングすれば十分です。その際に忘れてはならないことが「抽象化」です。components/templates/Articles/List/Cardcomponents/templates/Publishers/List/Cardが全く同じコンポーネントだという事に気がついたので、リファクタリングしてみます。Props 型定義の内訳は、それぞれ以下のとおりです。

type ArticlesListCardProps = {
  title: string;
  author: string;
  description: string;
}
type PublishersListCardProps = {
  title: string;
  publisher: string;
  body: string;
}

「author・publisher」「description・body」は同じ場所にレンダリングされる値です。「Articles」や「Publishers」などの文脈は、語彙レベルで「非横断的」ですね。そのため、横断的なものとなる様、コンポーネントの命名はもちろん、Propsも見直しが必要となります。

components/molecules/ListCard というコンポーネントなら、命名・場所ともに適切にみえますので、新設することにします。以下の様な Props が悪いのは明らかで、親のユースケースが Props だけでなく、内部実装にまで進入してしまっていることが伺えますね。

type ListCardProps = {
  title: string;
  author?: string;
  description?: string;
} | {
  title: string;
  publisher?: string;
  body?: string;
}

抽象化リファクタリングとは、既存コンポーネントの場所をただ移動するのではなく、見出された共通項のもと、利用しやすいものへ変更することです。

横断的コンポーネントは親のユースケースにあわせない

横断的コンポーネントとなった瞬間から、コンポーネントはどの様な親に利用されるのか知ることができません。そのため、コンポーネント自身がどの様な機能を提供するのか、主観をもつ必要があります。

type ListCardProps = {
  title: string;
  subTitle: string;
  main: string;
}

それぞれの Props をマッピングするのは親の責務です。マッピング作業が発生していない場合、親のユースケース(限定的コンテキスト)が、横断的コンテキストに漏れてしまっている兆候かもしれません。粒度の小さい atoms / molecules では Props 含め「抽象的な名前になっているか・親のユースケースに影響された名前になっていないか」は必ず確認しましょう。

なぜ初めから横断的コンポーネントにできないのか

フロントエンド実装は、デザイン成果物に依存しています。「デザインが全て出来上がっているから作ってお終い」ではない事がほとんどです。これは「締め切りまでにデザインが有った無かった」の話ではありません。サービスが成長するとともに、新しいコンポーネントが増えていくのは、デザイナーからしても予測可能なものですから。

限定的コンポーネントであったものが、サービスの成長に伴い横断的なものに昇華していく工程は不可避。つまり、継続的リファクタリングは不可避であるということです。モジュールシステムや静的型付けは、この様なリファクタリングを最大限サポートしてくれるでしょう。

Discussion