thasmto's blog

フロントエンドエンジニアやってます。プログラマーとして学んだことや考えていることを投稿するブログです。

コンポーネント設計の基礎

ソフトウェアコンポーネントについて、Wikipediaでは以下のように記載されています。

Wikipedia:ソフトウェアコンポーネント

事前に製作されたコンポーネント群を組み合わせて、電気製品や機械製品を作るようにソフトウェアを作れることを強調する。

このようにコンポーネント指向の開発ではいくつかのコンポーネントを組み合わせてサービスを開発します。
そのため、ひとつ一つのコンポーネントは様々な組み合わせに耐えうる設計をしなければいけません。
そのようなコンポーネントの設計を行う上での基礎的な考え方を紹介します。

コンポーネントにマージンやポジションなどの外側のレイアウトを設定しない

コンポーネントが持つ情報はコンポーネントの内側の情報だけにとどめることで、外側がどんな状況でも組み合わせやすくなります。

隣のコンポーネントと何pxスペースをあけるか、親コンポーネントの左端から何pxの位置にコンポーネントを置くかという、マージンやポジションはコンポーネント の外側の情報になるのでコンポーネントに持たせるべきではありません。

コンポーネントに情報を持たせるか迷った時は、情報がコンポーネントの内側に関することなのかという点は一つの指標になると思います。

コンポーネントに親コンポーネントの知識を持たせない

先ほども述べたとおり、コンポーネントが外側を知りすぎると良くありません。
コンポーネント→子コンポーネントの順番でコンポーネント を作成していると、
子が親の知識(コンポーネントの外の知識)を持っていることを前提にした「親<子」の関係になってしまいがちです。
この間違いをすると、特定の親コンポーネント でしか使えないコンポーネントになってしまいます。
コンポーネントを設計する際は、親コンポーネント が子コンポーネントの知識を包括する「親>子」の関係を意識したほうがいいです。

// Bad

const Parent = () => {
  const list = getList();
  const index = getIndex();
  
  return <div><Child list={list} index={index}  /></div>
}

// ✖︎ 子が親のstateを知りすぎている
const Child = ({list, index}) => {
  const item = list[index]
  return <div>{item}</div>
}
// Good

const Parent = () => {
  const list = getList();
  const index = getIndex();

  return <div><Child item={list[index]}  /></div>
}
  
// ○ 子は自分自身の必要最低限の情報のみ知っている
// itemさえあればどこでもコンポーネントを呼び出せる
const Child = ({item}) => {
  return <div>{item}</div>
}

インターフェースを統一する

コンポーネントを利用する人がコンポーネントについて学ばなければいけないことがあまりに多いと、それは利便性に欠けた設計と言えます。
利用者がコンポーネント名や利用シーンからある程度の使い方をイメージできなければ、間違った使われ方をしたり、呼び出されたコードを見ても何を意味しているかわかりづらく可読性の低いコードになってしまうので注意が必要です。

特に、同じような意味合いのデータやプロップスでもコンポーネントによって名前が違ったり、データの型が異なっていると利用者は混乱します。最低限、コンポーネント のインターフェースを統一することを意識しましょう。

簡単に始められることとして、既存のHTMLの要素のインターフェースを真似することをおすすめします。 例えば、ボタンコンポーネントを作成する場合は、HTMLのbutton要素のようにdisabled属性を受け取ってボタンを非活性に出来たり、タグでテキストを囲むとボタン内のテキストとして表示するといった具合です。

// buttonタグのインターフェースを利用できていない例
<Button isDisable text="進む" />
// buttonタグのインターフェースを利用している例
<Button disabled>進む</Button>

また、HTML要素も再利用可能なコンポーネントの一つとして捉えることができ、利用者がそれらを呼び出す際にHTML要素なのか、それとも作成されたコンポーネントなのか意識しなくてもいいような設計が望ましいと考えます。

そういった意味でもHTMLの要素がどういったインターフェースになっているか観察し、真似できるところがないか考えてみることは重要です。

粒度を意識する

コンポーネントはどれだけUIの情報を詰め込むかで粒度が変わってきます。 一つのコンポーネントで細かいUIの情報を多く含めすぎると、違いがほとんどないUIを作りたいとなったときでも、それらを構成するUIを再利用できず、ほとんど内容が変わらないコンポーネントを増やすか、無理矢理拡張性を持たせることになってしまいます。

再利用可能なコンポーネントにするためには、切り分けられる最小の単位までコンポーネント分割することです。小さく分割したコンポーネント群は、そうでないものに比べて多様な組み合わせが可能になります。

f:id:thasmto:20210822234601p:plain
コンポーネントの粒度を意識する

UIを分割したときコンポーネントの最小単位は機能ごとに変わってきます。 例えば、ボタンコンポーネントはbuttonタグとスタイルだけで構築可能ですが、 ヘッダーコンポーネントは各ページへのナビゲーションやロゴ、ハンバーガーメニューなどの構成要素を必要とします。 このコンポーネントごとの粒度の違いを意識し、整理整頓することで再利用可能なコンポーネントを開発しやすくなります。

また、メリットはそれだけではありません。 コンポーネントを適切な粒度に分割すると、それぞれのコンポーネントを修正した際の影響範囲の予測も容易になります。

粒度の小さいコンポーネントは、粒度が大きいコンポーネントの構成要素として再利用される場面が多くなります。それらに比べて粒度が大きいコンポーネントは再利用される場面は少なくなってきます。

ボタンなどの粒度の小さいコンポーネントを修正する場合は、その他のコンポーネントへの影響が出ないように意識する必要がありますが、粒度が大きいコンポーネントは使用される場面を特定しやすく、影響内容を正確に把握できるので、UIに影響がある修正も許容しやすくなります。

このようにコンポーネントを適切な粒度に切り離し、それぞれの影響範囲を予測しやすくなると、コンポーネントの修正や拡張をする際の判断を正確にできるようになります。

アトミックデザイン

https://atomicdesign.bradfrost.com/ コンポーネントの粒度の目安としてAtomic Designという方法論があります。

物質が原子や分子から構成されるように、コンポーネントを以下の5つの粒度に分類します。 Atoms(原子) Molecules(分子) Organisms(有機体) Templates(テンプレート) Pages(ページ)

詳細は割愛しますが、AtomicDesignは広く受け入れられている考え方なのでコンポーネントの粒度を考える際の目安として参考になると思います。

粒度を分類する際の注意点

粒度を分類する際に陥りやすい間違いとして、粒度を正しく分類できたとしても、そのコンポーネントの中で粒度の小さい部分まで定義してしまうことがあります。
例えば、ヘッダーコンポーネントをOrganismsに分類して作ったとしても、そのヘッダーの中でMoleculesやAtomsなどの小さい粒度のUIまで定義してしまうといったことです。
なるべくコンポーネント内の小さい粒度の実装は別のコンポーネントとして切り出すべきで、小さい粒度の実装を含めるとしても一つか二つ下の粒度(TemplatesならOrganismsとMoleculesまで)にとどめるべきと考えます。

また、階層の順番が逆になってしまっているコンポーネントもたまに見かけます。
Templatesのコンポーネントの中でPagesのコンポーネントを呼び出していたり、
Moleculesの中コンポーネントの中でOrganismsのコンポーネントを呼び出すといったことです。
実装しているコンポーネントの粒度だけでなく、その中で呼び出すコンポーネントの粒度も意識しなければいけません。