合成パターン(Composition pattern)で見るReactコンポーネント設計の核心

designPatternprinciples

この記事を読んだ後の状態

  • Composition Patternが何かを理解した。
  • Composition PatternがなぜReactの核心的な開発原理なのか理解した。
  • Composition PatternがReactでどのように活用されるか把握した。

はじめに

ある知識を勉強する時、最初から理解できれば良いのだが、後になって理解できることが多いようだ。

私にとってはComposition patternがそのようなケースだった。

Reactで使われる合成という概念の核心であるため頻繁に接してきたが、関数ベースのReactを通じて日常的に使用していながらも、あまりピンとこなかったようだ。

今回プロジェクトを進める中でいくつかのコンポーネントを直接実装しながら、この概念の意味が💡!と理解できるようになった。

今から私が理解した概念を整理しようと思う。

Composition patternとは?

コンポジションパターン(Composition Pattern)は別名合成パターンとも呼ばれる。

オブジェクト指向プログラミング(OOP)に関連したデザインパターン(Design pattern)の一種で、複合オブジェクト(Composition)単一オブジェクト(Leaf)を同じコンポーネント(Component)として扱い、クライアントにこの2つを区別せずに同じインターフェースを基に使用させる構造である。

説明よりも例を先に見た方が理解しやすいだろう。

コンポジションパターンの一例:ファイルシステム構造
コンポジションパターンの一例:ファイルシステム構造

コンポジションパターンを話す時、通常ファイルシステム構造を例としてよく挙げる。

ファイルとディレクトリはどちらも通常ダブルクリックまたは開くを通じて使用できる。

両者は対象が異なるだけで、使用するユーザーの立場からは同じ動作を行う。

これがまさにコンポジションパターンである。

ここで複合体パターンは、ファイルとディレクトリを同じインターフェースを通じて操作することが目的である。

Loading diagram...

上記のような特徴を持っている。上のダイアグラムをよく見ると、ComponentとComposition(Directory)がmany:1の関係を持っていることがわかる。

整理すると以下のようになる。

  • Component : Interfaceとして、LeafとComponentを一つにまとめ、これらが実行すべき共通動作を定義
  • Composition : 複合オブジェクト、LeafとCompositionを内部に保存し管理する役割
    • Component実装体(Leaf, Composition)を内部でリストやオブジェクトなどで管理
    • add / remove関数は内部で単一コンポーネント実装体を保存または削除する役割を実行
    • operateを実行時、内部のリストを巡回しながら各オブジェクトのoperateを実行
  • Leaf : 単一オブジェクト、ファイルのように単純に内容を表示する役割
    • Componentのインターフェースであるoperateは、Leafで実行時に適切な値を返す

核心はComponentという抽象メソッドを定義し、Compositionでこれを実装する実装体を配列で受け取り再帰(Recursive)的に管理することである。

簡単にまとめた文章だが、これに関してより詳しく説明された記事があるので、追加学習が必要であれば以下の記事を見ると良いだろう。

複合体(Composite)パターン - 完璧マスター

代表的な合成コンポーネントの使用例

先ほど、合成パターンとは何かを理解した。

では今から議論すべきことは、果たしてフロントエンドでどのように活用されるかということである。

これについては、もう少し具体的に入って、モダンReactでどのように活用されるかを話したいと思う。

最近のフロントエンドトレンドでReactは本当に欠かせない存在になったので...一度見てみよう。

Reactと合成コンポーネント

Reactは実際にコンポーネントの合成を通じて複雑なUI/UXを実装できるように設計・実装された。

これに対する根拠資料は簡単に見つけることができ、以下のようなものがある。

合成(Composition) vs 継承(Inheritance)

Reactの公式ドキュメントだが、最初から合成を推奨していることがわかる。

Reactで考える

これは比較的最新のドキュメントだが、ここでも合成コンポーネント方式を基にチュートリアルを進めていることがわかる。

具体的な事例

Reactで合成コンポーネントパターンが使われていることもわかったので、今度はもう少し具体的な要素を見てみよう。

Reactの設計および開発哲学の一つであるため、事例は本当に多いが、これを簡単に理解できる事例は4つのようだ。

  1. Render Props
  2. Children Props
  3. Higher-Order Components (HOC)
  4. Compound Components

1番はclass文法に関連して話したいと思い、2、3、4番はfunction文法に基づいて話したいと思う。

1. Render Props

Render Propsは、コンポーネントが関数形式のpropを通じてレンダリング内容を制御できるようにするパターンである。

これによりコンポーネント間のロジックを共有でき、それに伴う例は以下のようになる。

JSX

Render Propsパターンを使用してDataFetcherコンポーネントがデータを取得するロジックをカプセル化した姿である。

JSX

ここで確認できるように、レンダリング方式は親が定義できるようにする方法である。

クラス文法が不慣れかもしれないと思い、言っておくと、Reactのclass文法ではrender()functionreturnと同じ役割を果たす。

そのため上の構造を見ると、引数として受け取り、これをrender()に入れてレンダリングする方式を採用している。

つまり、componentoperate()render()であると考えると良いだろう。

そして、もし上記の技法を関数コンポーネントで使用したい場合は、同様にpropsにコンポーネントを作成して渡せば良い。

これに関する話は2番でするようにする。

2. Children Props

Reactコンポーネントはchildren propを通じて他のコンポーネントを含めることができる。

Reactを触ったことがあれば多く使用した方式で、これにより親コンポーネントが子コンポーネントを組み合わせて複雑なUIを構成できる。

JSX

この例でCardコンポーネントはCardHeaderCardBodyコンポーネントを含めて一つのカードUIを構成する。

これにより各部分を独立して再利用し管理できるようになる。

実はこれも考えてみれば1番と大きな違いはない。分割代入で{children}と表記しているだけで、元々はprops.childrenと表現して使用可能だからである。

3. Higher-Order Components (HOC)

HOCはコンポーネントを引数として受け取り、新しいコンポーネントを返す関数である。

高階コンポーネントとも呼ばれ、JSの高階関数から持ってきた方法である。

高階関数はJavaScriptの根本原理であるため...これについては後で別途扱うことにする。

とにかく、HOCを通じてコンポーネントの機能を拡張したり、共通ロジックを再利用できる。

JSX

使用例は上記のようになる。

HOCを使用するとButtonコンポーネントの機能は維持したまま、ロギング機能を追加でき、他のコンポーネントにも同じHOCを適用できる。

これに対する活用技法のもう一つの例としてはエラー処理がある。

4. Compound Components

Compound Componentsパターンは、関連する複数のコンポーネントを組み合わせて一つの複合コンポーネントを構成する方法である。

これにより、より直感的で宣言的なAPIを提供できる。

JSX

この例でTabsコンポーネントは複数のTabコンポーネントを子として受け取り、アクティブなタブを管理しレンダリングしている。

これによりタブUIを簡単に構成できる。

このように合成パターンを使用する時の利点は以下のようになる。

  • 再利用性向上: コンポーネントを独立して設計し、様々な状況で再利用できる。
  • 柔軟性増大: コンポーネントの組み合わせ方式を通じて様々なUIを動的に構成できる。
  • 保守容易: 各コンポーネントが独立して動作するため、修正や拡張が容易になる。
  • 可読性向上: コンポーネントの役割が明確に分離されてコードの可読性が高まる。

しかし、ただ良いものではない。以下のような注意事項が存在する。

  • 過度な抽象化を避ける: 合成パターンを乱用するとコンポーネント階層が過度に複雑になる可能性がある。適切なレベルで合成を使用することが重要である。
  • 明確なAPI設計: コンポーネント間の相互作用が明確でなければならない。特にCompound Componentsを使用する時は、親と子の間のインターフェースを明確に定義しなければならない。
  • パフォーマンス考慮: 合成パターンを使用する時、不要な再レンダリングが発生しないように最適化が必要な場合がある。React.memouseCallbackなどを活用してパフォーマンスを最適化できる。

まとめ

ここまでComposition Patternとは何かの原理を把握し、これがReactでどのように適用されるかを見てきた。

先に紹介した4つの事例以外にも、このパターンは本当に頻繁に使用されている。

もう少し深く入って、紹介されたもの以外にもどのような問題にこのパターンを活用できるかを考えてみれば、この記事に加えて追加的な成長を遂げられるのではないだろうか。