AtomicDesign 境界線のひき方
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/Card
とcomponents/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