概要

凝集度: 1つのモジュールがどれだけ1つの責務に集中できているか

結合度: モジュール同士の依存関係がどれだけ強いか

理想的なのは高凝集・低結合

つまり理想なのは、

  • 1つのモジュールが1つの責務に集中している
  • モジュール同士の依存関係が弱い

凝集度

モジュールが1つの責務に集中している度合い

1つのモジュール内に複数のメソッドがあってもそれが同じ責務に関するものであれば高凝集になる

凝集度にはレベルがある。(レベルが上がるほど高い)

LV1. 偶発的凝集(最悪)

一番悪い例

関連性のない要素が集まっている

なんも関連性がないものが集まっている状態

LV2. 論理的凝集

ラベリングの一部が同じであるが、関連性のない要素が集まっている

〇〇関連の要素を集めたシリーズ。

これがエスカレートすると、なんでも集まっちゃったなんでもモジュールが生まれてしまい、結局スコープが少し狭いだけの偶発的凝集になってしまう。

LV3. 時間的凝集

実行タイミングが同じものが集まっている。

初期化処理などでありがち。 あとは、ボタンクリックした時のhandleClickの中とかもそうかも。

例えば、

fn initialize() {
  loadConfig();
  initializeDb();
  loadCache();
  loadUser();
}

これがエスカレートすると結局そのタイミングにすることがどんどん増えていって、なんでもモジュールになってしまう。

LV4. 手順的凝集

実行時に順番に実行されるものが集まっている。

時間的凝集に近いが、そのモジュール内での実行順が大事になっているかどうかがポイント。

例えば、

fn login(user_id: i32) {
  check_user_exists(user_id);
  create_session(user_id);
  redirect_to_user_page(user_id);
}

この例は同じタイミングで特定の順番での実行が大事だが、 sessionを作成するのと、リダイレクトするのは、関連性が低い。

これがエスカレートすると、結局なんでもモジュールになってしまい、テストがしづらくなる

LV5. 通信的凝集

同じデータに関する処理を集めた状態。

手順的凝集や時間的凝集と似ているが、同じデータを扱う処理が集まっていることがポイント。

例えば、

fn handle_order(food: Food) {
  log_order(food);
  send_order_to_kitchen(food);
  add_order_to_receipt(food);
  think_about_food(food);
}

この例は、順番もタイミングも大事にしてなくて、とりあえずfoodに関する処理が集まっている状態。

Railsはこれに陥りやすい

例えば: Userに関するメソッドを集めたmodelは、Userに関するものなんでも集めてしまうことが多い。

役割がバラバラになる + 影響範囲が大きいため、 再利用・テストやメンテナンスがしにくくなる。

LV6. 逐次的凝集

手順の中で出力が次の入力として使われるような構造になったモジュール。

手順的凝集の中で、さらに出力と入力の関係があるものが逐次的凝集。

例えば、

fn login(user_id: i32) {
  let user = fetch_user(user_id);
  let user = update_last_login_time(user);
  create_session(user);
}

この例のように手順をまとめる用途なら問題ない。(広く見れば同じ責務に関するものが集まっているとも言える) ただ、update_last_login_timecreate_sessionと狭く見れば責務が違うものが集まっているとも言える。

責務が違うものが集まってると、他の低いLVの凝集と同じようにテストがしづらくなる

LV7. 機能的凝集(完成系)

一番理想的で、1つのモジュールが明確に1つの責務に集中している状態。

// これは逐次的凝集
fn handle_create_session(user_id: i32) {
  validate_user_id(user_id);
  let user = fetch_user(user_id);
  let user = update_last_login_time(user);
  create_session(user); 
}

// これは機能的凝集
fn validate_user_id(user_id: i32) {
  // user_idのバリデーションをする処理
}

// これは機能的凝集
fn fetch_user(user_id: i32) -> User {
  // user_idからユーザーを取得する処理
}

// これは機能的凝集
fn update_last_login_time(user: User) -> User {
  // ユーザーの最終ログイン時間を更新する処理
}

// これは機能的凝集
fn create_session(user: User) {
  // セッションを作成する処理
}

handle_create_session以外は機能的凝集で、1つの責務に集中している状態。 それぞれが単一の責任のため可読性が高いし、テストもしやすい。

機能の一部を変更しても他の機能に影響を与えないため、保守性も高い。

また、細かく分離されているので再利用もしやすい。

高凝集のいいところ

  • 単一の責任のため可読性が高い
  • 単一の責任のためテストしやすい
  • 単一の責任のため保守性が高い(メンテナンス性が高い)
  • 単一の責任のため再利用性が高い

結合度

モジュール同士がどれだけ強く依存しているかの度合い

結合度にもレベルがある。(レベルが上がるほど低い)

LV1. 内容結合(最悪)

モジュール同士の結びつきが強い。

モジュールが別のモジュールの内部にアクセスする状態。

例えば、

struct User {
  id: i32,
  name: String,
}

impl User {
  pub fn get_name(&self) -> &str {
    &self.name
  }
}

fn update_user_name(user: &mut User, new_name: String) {
  user.name = new_name;
}

この例はupdate_user_nameUserの内部にアクセスしている状態。

implにupdate_nameを定義して、update_user_nameをなくすことで内容結合を解消できる。

LV2. 共通結合

別々のモジュールが同じグローバルの状態に依存している状態。

グローバル変数の状態によって挙動が変わったりするため、思わぬ挙動を引き起こす可能性 → バグ

フロントで言うとRedux, Vuexのstoreとかもこれに当てはまるかも。

LV3. 外部結合

外部の直接的な値ではなく、ルール・フォーマットなどに依存している結合。

APIレスポンスや外部のファイルを読むときに発生しやすい。

// 実務で結構ありそうな形式を再現するためにTypeScriptをここだけ採用
const fetchUser = async () => {
  const response = await fetch("/api/user");
  const data = await response.json();
  return data.user; // APIのレスポンスのフォーマットに依存している状態  
}

このコードの場合は、APIのレスポンスのフォーマットに依存している状態。

レスポンスの形式が変わったり、APIのエンドポイントが変わったりすると、fetchUserのコードも変更する必要がある。

APIのレスポンスとUserの構造を変換するための関数を作ることで、外部結合を解消できる。(Factoryパターンなどもこの辺りの解消に役立つ)

LV4. 制御結合

実行する処理の内容が外部のフラグやモードに依存している状態。

例えば、

fn main() {
  let user = User { id: 1, name: "Alice".to_string() };
  saveUser(&user, true); // 更新
  saveUser(&user, false); // 作成
}

fn saveUser(user: &User, is_update: bool) {
  if is_update {
    // update user
  } else {
    // create user
  }
}

この例はsaveUseris_updateという外部のフラグに依存している状態。

論理的凝集の一種とも言える。

フラグがさらに増えていくと、なんの処理をするのかがわかりづらくなってしまう

この場合はcreateUserupdateUserの二つの関数に分けることで外部結合を解消できる。

LV5. スタンプ結合

構造体の一部を用いて処理をしたいのに、全部渡してしまっている状態。

不要な情報も渡してしまうため、モジュール同士の結びつきが強くなってしまう。

構造体の構造が変わったときに、必要ないとしても問題がないかを確認しなければならないコストが発生する。

struct User {
  id: i32,
  name: String,
  email: String,  
}

fn send_user_email(user: &User) {
  send_email(user.email, USER_EMAIL_TEXT);
}

この例は、sendUserEmailUserの構造体の一部であるemailを使いたいのに、構造体全体を渡してしまっている状態。

もしUserの構造が変わったときに、sendUserEmailに影響がないかを確認する必要がある。

send_user_emailの引数をemail: &strにすることで、スタンプ結合を解消できる。

LV6. データ結合

引数として必要なデータを渡している状態。 外部から値を渡さなければいけない状態でもっとも結合度が低い状態。

例えば、

struct User {
  id: i32,
  name: String,
  email: String,  
}

fn send_user_email(email: &str) {
  send_email(email, USER_EMAIL_TEXT);
}  

ただどうしても、emailには依存してしまうため、Userの構造が変わったときに、emailがなくなっていないかを確認する必要がある。

LV7. メッセージ結合(理想)

引数のないメソッド。(メソッド内でも何にも依存していない)

低結合のいいところ

  • モジュールが独立する
  • 独立した処理になるのでテストしやすい
  • 独立した処理なので変更がしやすい(保守性が高い)
  • チーム開発しやすい(依存がないので、他の人のコードに影響されない)
  • スケーラビリティが高い(モジュールを追加しても、既存のモジュールに影響がない)

参考