[フロントエンド] うわっ…Componentの凝集度、低すぎ?


[追記 2021/4/26] Storybookを導入して凝集度を向上させる記事を書きました。

0. はじめに

有名OSSのコミッターから、コピペで動かすマンまで、彼らは等しくプログラマと呼ばれます。10xプログラマという言葉があるように、同じプログラマでもその生産性には天地ほどの開きがあります。

プログラマの生産性は、1968年のSackmanらの研究以来、ソフトウェア工学でも熱い研究テーマの一つですが、未だにプログラマの生産性を測る指標は確立されていません。

一方、広木大地氏は自著「エンジニアリング組織論への招待」で、エンジニアリングを不確実性を削減する行為と定義しました。プログラミング能力を測る重要な尺度として、モジュールの凝集度があります。高い凝集度で設計しコーディングされたモジュールは、見通しがよく、再利用可能で、バグが少ない…つまり不確実性が少ない状態と言えるでしょう。

システムが指数関数的に複雑化し続ける昨今、凝集度という概念は、1つのクラスからインフラ構成に至るまで、ソフトウェア・エンジニアリングの全てのレイヤーで必要となる基本教養です。凝集度は言語に依存しない概念ですが、ここではWebのフロントエンドを題材に、凝集度を向上させる方法を実例と共に紹介します。

1. 対象読者

下記の人たちに宛てて書きます。

  • 凝集度が何かよく知らない方
  • 凝集度の高い設計にいまいち自信のない方
  • クソコードを書いてスヤスヤ眠る毎日を過ごしていた過去の私

2. 類語

言葉の意味を知りたいとき、国語辞典より類語辞典を引いたほうが理解の助けになる場合があります。ここでは凝集度そのものの説明の前に、凝集度の類語をいくつか紹介します。

2-1. UNIX哲学 - Do one thing well

UNIXはソフトウェア界のバイキングたちが昼夜を問わず開発を進めている大規模なOSSです。そのUNIX開発を背骨のように支える思想を総称しUNIX哲学と呼びます。

UNIX哲学の中でも、様々なプロジェクトに適用できる思想が Do one thing well=1つのことを上手くやれ です。

例えば、次のUNIXコマンドは今いるディレクトリの.jsonファイルを個数を出力します。

$ ls
bar.json    foo.json    fuga.gif    hoge.txt    piyo.json
$ ls -l | grep '\.json$' | wc -l
3

UNIXではプログラムを小さな粒度に保つべきという掟があります。複雑な処理を行いたいときは、上の例のように、それら小さなプログラムをパイプ|で連結して実現します。

つまり個々のプログラムは、小さな責任を冴えたやり方で果たせ、すなわちDo one thing wellであれという考えです。

2-2. KISS原則

Keep it simple, stupid=シンプルにしておけ、バカ、もしくはKeep it short and simple=簡潔かつシンプルしておけのアクロニムです。

一般にコードは書かれる時間よりも読まれる時間の方が長いです。したがって、コーディングに時間を掛けてでも、モジュールはリーダブルである必要があります。

また一番美しいコードは0行のコードだという極論が示すように、設計のシンプルさはシステムの堅牢性と明白な因果関係があります。スペースシャトルよりもソユーズの耐用年数が長いのも、AK-47が世界で最も使われる銃であるのも、これらの設計がシンプルであることと無関係ではありません。

3. 凝集度

上で述べた類語に共通することはなんでしょうか。それはモジュールの責任を減らすという考え方です。

Do one thing wellKISS原則を現実世界のコードに反映させる具体的な指標が凝集度です。

ソフトウェアの複合/構造化設計」によれば、凝集度はその巧拙のレベルにより次の7つに分かれます。

  1. 偶発的凝集(最悪)
  2. 論理的凝集
  3. 時間的凝集
  4. 手続き的凝集
  5. 通信的凝集
  6. 逐次的凝集
  7. 機能的凝集(最良)

しかしこの7つのレベルは細分化されすぎており境界が曖昧です。知っておくことは決して無駄ではありませんが、憶えていてもそれほど実務では役に立ちません。

また凝集度の低さを定量的に測るLCOM*という指標があり、次式で表されます。

LCOM* = \frac{\frac{1}{a}\sum_{j=1}^{a}\mu(A_j)-m}{1-m}
変数 説明
$a$ クラスのメンバ変数の個数
$A_j$ クラスのj番目のメンバ変数
$m$ クラスのメソッドの個数
$\mu(A_j)$ $A_j$にアクセスしているメソッドの個数

しかしLCOM*は古き良きオブジェクト指向言語のクラス設計には有効ですが、特に昨今のフロントエンドでデファクトスタンダードとされる設計手法から見ると、時代遅れの感は否めません。

したがってこの記事では、現実世界のプロジェクトで如何に凝集度が低下するか、具体例を使って示します。

4. 凝集度が下がる瞬間

フロントエンド開発においてComponentの凝集度を上げると言った場合、Componentの責任を可能な限り小さく保つことを指します。

例えばユーザーのプロフィールを表示する次のようなProfile Componentを考えます。

const Profile = ({user: UserModel}) => {
  return (
    <>
      <img src={user.avatar} />
      <h2>Hi, I'm {user.name}.</h2>
    <>
  )
}

userという単一の引数を持ったシンプルなComponentで、プロフィールを表示するという責任に集中しています。よって凝集度は最良と言えます。

しかしここで「プロフィールと一緒に友達のリストを表示する」という要件が追加されたとします。Profile Componentは次のようになりました。

const Profile = ({user: UserModel}) => {
  // 友達をロードして
  const friends: FriendModel[] = loadFriends(user.id)
  // 友達リストのNodeを作る
  const friendsNode = (
    <ul>
      {friends.map(friend => <li key={friend.id}>{friend.name}</li>)}
    </ul>
  )

  return (
    <>
      <img src={user.avatar} />
      <h2>Hi, I'm {user.name}.</h2>
      {friendsNode}
    <>
  )
}

しかしあなたはProfile Componentが既に複数の場所から呼ばれており、友達を表示したくないケースがあることに気が付きます。そのため友達リストの表示をコントロールできるよう、Profile Componentに分岐処理を加えました。

const Profile = ({user: UserModel, shouldShowFriends: bool}) => {
  // 友達リストのNodeを生成する関数
  const showFriends = () => {
    // 非表示ならreturn null
    if (!shouldShowFriends) return null

    // 表示するなら友達をロードしてNodeを生成
    const friends: FriendModel[] = loadFriends(user.id)
    return (
      <ul>
        {friends.map(friend => <li key={friend.id}>{friend.name}</li>)}
      </ul>
    )
  }

  return (
    <>
      <img src={user.avatar} />
      <h2>Hi, I'm {user.name}.</h2>
      {showFriends()}
    <>
  )
}

引数に依存した分岐処理が加わりました。凝集度が低下した瞬間です。

Profile Componentは、プロフィールを表示するという唯一の責任を果たす美しいモジュールでした。それがいまや分岐処理を加えられ、友達リストを表示するという新たな責任を負ってしまいました。

5. 凝集度を高く保つ

それでは「友達リストを表示する」という要件に対して、どのようなComponentを書けば良かったのでしょうか。

当然答えは友達リストを表示するという単一の責任だけを負った新たなComponentを作る、です。Profile Componentからコードを分離し、FriendsというCompnentを作ります。

const Friends = ({user: UserModel}) => {
  const friends: FriendModel[] = loadFriends(user.id)

  return (
    <ul>
      {friends.map(friend => <li key={friend.id}>{friend.name}</li>)}
    </ul>
  )
}

そしてあとは、Profile Componentを呼んでいた親Componentに、Friends Componentを追加してあげれば良いのです。

// 親Componentから個々のComponentをコール
<Profile user={user} />
<Friends user={user} />

これで個々のComponentの凝集度を高く保ったまま、要件に応えることができました。

またFriends Componentを分離したことで、「友達リストを表示する」という機能に対して、以後は同じFriends Componentを再利用すれば良くなった点も重要です。

Componentにコードを書き足す前に、その修正によりComponentの責任を増やすことにならないか、自分に問いかけてみましょう。もし答えがYESなら、それはComponentを分割するタイミングであるはずです。

6. 結合度

凝集度とセットで語られる概念として結合度があります。これらは混同されやすい概念ですが、図示すると分かりやすいです。図の中の円は、引数、メンバ変数、メンバ関数といった要素を示します。

凝集度の対象はモジュール単体です。モジュールの担う責任の少なさ、要素の少なさ、要素同士の関連度によって決定します。高いほど善です。

一方、結合度の対象はモジュール同士の関連です。モジュール同士の結びつきの強さを表します。低いほど善です。

ただし凝集度と結合度は反比例の関係にあります。高い凝集度を意識し書かれたモジュールは、自ずと低い結合度になります。結合度への意識が必要なケースもありますが、フロントエンド開発においては、凝集度さえ意識できていれば問題ないというのが今の所の結論です。

7. おわりに

ソフトウェア・エンジニアリングの世界は流行り廃りが激しい一方、どのようなプロジェクトでも有用な教養があります。そのうちの一つが凝集度という古くから存在する尺度です。

特に昨今のシステムは指数関数的に複雑化しており、モジュールの凝集度を高く保つことはほぼ全ての職業エンジニアに求められます。

Microserviceアーキテクチャや関数型言語といったトレンドの底流にあるのも、凝集度と同じく、小さな責任を冴えたやり方で果たすという考えです。

Twitter: @aki202