iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

凝集度と結合度について調べてみた(part2 結合度編)

こんにちは。iimonでエンジニアをしているhayashiと申します。

普段は主に拡張機能を開発しております。

「綺麗なコードとは何か」ということについて調べていて、前回は凝集度について解説したので、

https://tech.iimon.co.jp/entry/2025/07/15/160000

今回は結合度について解説していきたいと思います。

結合度とは

結合度とは一言で表すと、モジュール同士の依存関係の強さを表す指標です。

密結合(結合度が高い)だと変更に弱く、再利用性が低く、テストし辛かったりします。

逆に疎結合(結合度が低い)だと、柔軟で保守性の高い設計になるとされています。

  • 凝集度
    • モジュール内での評価指標
    • 高凝集だと堅牢性、信頼性、再利用性、可読性が向上
  • 結合度
    • モジュール間での評価指標
    • 疎結合だと可読性や保守性が高い

上記の通り凝集度はモジュール内での評価指標で結合度モジュール間での評価指標です。

結合度は凝集度と相関関係があり、モジュールにその役割を果たす為のコードが凝集していれば、他のモジュールに頼らなくて済む為、凝集度が高くなれば、結合度は低くなる傾向があるとされています。

  • 高凝集 → 自分の責務がはっきり

    → 必要な情報や処理が内部にまとまる

    → 他モジュールへの依存が減る

    → 結合度が低くなる

  • 低凝集 → 責務が散らかる

    → 必要な情報や処理を外から取ってくる

    → 他モジュールとの依存が増える

    結合度が高くなる

上記のように凝集度が高いというのは、モジュール内の機能やデータが「同じ目的のために密接に関連している」状態です。

凝集度を上げれば結合度は下がる傾向にありますが、あくまで傾向とされていて、

それも完全ではないみたいなので

結合の種類を知り、高凝集・疎結合を目指せば堅牢性、信頼性、再利用性、可読性、そして保守性の高いコードが書けるようになりそうです。

密結合の場合

モジュール間の依存が高いと具体的に以下の問題が起こります。

  • 変更の波及範囲が大きい
    • 一つのモジュールを変更すると、他の多くのモジュールに影響が及ぶ
  • テストが書きにくい
    • 単体テストを書く際に、多くの依存関係をモックする必要がある
  • 再利用性が低い
    • 特定の動作する環境や前提条件に強く依存しているため、他の場所で使い回し辛い
  • 認知負荷が高い
    • 他のモジュールとの結合度が高いので、そのモジュールを理解するために、関連する他のモジュールも理解する必要がある

結合度は以下の7種類に分類されます。

結合度の種類

内部結合

内部結合とはあるモジュールが他のモジュールの内部実装(変数・処理)に直接依存している状態のことを言います。

privateにしている変数やメソッドにアクセスを行えてしまっている状態です。

例[1]

クラスのprivate変数に直接アクセス

[User.ts]

export class User {
  private data: { name: string; email: string; address: string };

  constructor(name: string, email: string, address: string) {
    this.data = { name, email, address };
  }
    
  .
  .
  .
  他の処理
}

[Hoge.ts]

import { User } from './User';

const user = new User("Alice", "alice@example.com", "Tokyo")
// anyとしているので内部メソッドにもアクセスできてしまう。
// 1,インスタンス変数にアクセスしている。
const address = (user as any ).data.address;

[例2]

他モジュールのprivateメソッドを直接呼び出す

[Calculator.ts]

export class Calculator {
 
  addDoubled(a: number, b: number) {
    return this.double(a) + this.double(b); 
  }
  
   private double(x: number) { 
      return x * 2;
   }
}

[Fuga.ts]

import { Calculator } from './Calculator';
const calc = new Calculator();
console.log((calc as any).double(5)); // private を直接呼ぶ

内部結合は以下の理由で最悪と言われています。

  • カプセル化の破壊
    • private は本来、クラスやモジュール内部の実装詳細を隠し、外部から直接触れられないようにするための仕組みです。これが破られると、内部の状態や動作を外部から勝手に操作できてしまい、設計の意図が崩れます。
  • 内部状態の不整合や破壊
    • 外部から private にアクセスし不適切な値を設定したり、予期しないタイミングでメソッドを呼ぶと、オブジェクトの状態が壊れたり一貫性が失われます。結果として不具合の原因になりますし、型付けをしている意味がなくなります。
  • メンテナンス性・保守性の低下
    • 内部実装を勝手に使われると、開発者が実装を変更した際にどこで使われているか追いづらくなり、修正が困難になります。結果としてバグの発生や改修コスト増大を招きます。
  • セキュリティリスクの増大
    • 内部の重要な処理やデータに外部からアクセスできてしまうと、悪意のあるコードから操作されるリスクが高まります。特にユーザーデータや認証関連は危険。

共通結合

[例1]

共通結合とはグローバル変数や共有状態を複数のモジュールが参照・変更できる状態です。

[SharedConfig.ts]

// 共有設定オブジェクトを複数クラスで変更
export const sharedConfig = {
    maxImages: 5,
    allowedCategories: ['外観', '周辺', 'その他'],
    uploadTimeout: 3000,
};

[ImageValidator.ts]

import { sharedConfig } from './sharedConfig';

class ImageValidator {
    validate(images: ImageType[]) {
        if (images.length > sharedConfig.maxImages) {
            // maxImagesを変更してしまう
            sharedConfig.maxImages = images.length;
        }
        return true;
    }
}

[CategoryFilter.ts]

import { sharedConfig } from './sharedConfig';

class CategoryFilter {
    filter(category: string) {
        if (!sharedConfig.allowedCategories.includes(category)) {
            // allowedCategoriesを変更してしまう
            sharedConfig.allowedCategories.push(category);
        }
    }
}

共通結合は以下の理由でとても悪いとされています。

  • どこで値が変更されるか分からない。
  • 一つの箇所を直した際にグローバルなので、あらゆるところに影響が及ぶ可能性が高く保守性が低い。
  • 並行処理で競合状態が発生する可能性がある。

外部結合

外部結合とは、外部(データベース、ファイル、外部API、デバイスなど)に複数のモジュールが依存している状態にいることを指します。

また、外部データ形式に依存することも外部結合となるようです。

例えば以下のLogin.tsはログインが上手くいったら、localStorageにaccess_tokenを保存してuser_infoをreturnするコードで、AuthStatus.tsはtokenが有効か確認するコードです。

[Login.ts]

// ログイン
export const loginUser = async (email: string, password: string) => {
  const response = await fetch('https://api.example.com/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password })
  });

  const data = await response.json();

  if (data.success) {
    localStorage.setItem('token', data.data.access_token);
    // user_infoオブジェクトを、そのまま返す。
    // 外部APIの具体的なレスポンス形式に依存
    return data.data.user_info; 
  }
  throw new Error(data.message);
};

[login.tsレスポンスデータ形式]

// 成功時のレスポンス
{
  "success": true,
  "data": {
    "user_info": {
      "id": "123",
      "email": "user@example.com", 
      "full_name": "田中太郎",
      "profile_image": "https://...",
      "created_at": "2023-01-01",
      "last_login": "2024-01-01"
    },
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expires_in": 3600
  },
  "message": "ログイン成功"
}

// 失敗時のレスポンス
{
  "success": false,
  "data": null,
  "message": "メールアドレスまたはパスワードが間違っています"
}

[AuthStatus.ts]

// tokenが有効か確認(tokenが改竄されていないかなどもcheck)
export const checkAuthStatus = async () => {
  const token = localStorage.getItem('token');
  if (!token) return false;

  const response = await fetch('https://api.example.com/auth/status', {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${token}` }
  });

  const data = await response.json();
  // data.valid→tokenが有効化どうか
  return data.success && data.data.valid;
};

[tokenAuth.tsレスポンスデータ形式]

{
  "success": true,
  "data": {
    "valid": true,        // boolean: トークンが有効かどうか
    "expires_at": "2024-12-31T23:59:59Z",  // オプション: 有効期限
    "user_id": "123"      // オプション: ユーザーID
  }
}

// または無効な場合
{
  "success": true,
  "data": {
    "valid": false,       // boolean: 無効
    "reason": "expired"   // オプション: 無効な理由
  }
}

これは以下のような問題があり、保守性が低いとされています。

  • 別々のモジュールでhttps://api.example.com/auth の同じapiを叩いている為、同じようなfetch処理が複数箇所に散在していて、そのようなモジュールが増えれば増えるほど管理コストが上がります。

      const response = await fetch('https://api.example.com/auth/...', {
        headers: { 'Authorization': `Bearer ${token}` }
      });
    
  • return data.data.user_infoのように外部から返ってきたデータオブジェクトを、そのまま返しているので呼び出し元でres.user_info のように外部で作られたオブジェクトにアクセスしないといけないので結合度が上がってしまっている。それにより、仮にuserInfoの中身を参照している呼び出し元が多ければ多いほど、user_info影響範囲が大きくなります。

    [モジュールA]

      const userInfo = await loginUser('email', 'password')
    
      const funcA() {
          const id = userInfo.id
          // idを使った処理
      }
    
      const funcB() {
          const id = userInfo.id
          // idを使った処理
      }
    
      // 仮にuserInfoのオブジェクトのkeyがid→userIdとなったら、
      // 参照しているモジュールの数だけ書き換えないといけない。
    

制御結合

制御結合とはあるモジュールが他のモジュールに「処理の流れ(制御)」を指示する情報を渡す形の結合です。

つまり、「このフラグが true ならこの処理をして」「false なら別の処理をして」というように、

呼び出し側が処理の詳細な流れを決めてしまっている状態です。

特徴

  • 制御用のフラグやコードを引数として渡す。
  • 呼び出し元が呼び出し先の内部処理を知っていないと使えない。
  • モジュールの独立性が低くなる。
  • 内部の処理仕様が変更されると、呼び出し元も修正が必要になる可能性が高い。

[例1]

以下は渡されたmodeによって、dataのハンドリング形式を変えてます。

[DataFormat.ts]

export default class DataFormat {
  process(data: string[], mode: string) {
    if (mode === 'csv') {
      // csv format処理...
    } else if (mode === 'json') {
      // json format処理...
    } else if (mode === 'xml') {
      // xml format処理...
    } else if (mode === 'text') {
      // text Format処理...
    }
    throw new Error('Unknown mode');
  }
}

[hogehoge.ts]

import DataFormat from './DataFormat';
// 呼び出し側が内部の処理を知っている必要がある
const processor = new DataFormat();
const csvResult = processor.process(['a', 'b', 'c'], 'csv');     // 制御結合
const jsonResult = processor.process(['a', 'b', 'c'], 'json');   // 制御結合

上記の制御結合は以下の理由で改善点があるとされています。

  • 呼び出し元が内部仕様を知っていないと使えない
  • 内部も単一責任になっていない

結合度はモジュール間の依存関係の強さを表すので、呼び出した側のメソッドと呼び出された側のメソッドの依存関係となります。

制御結合で呼び出されるメソッドはメソッド自体も論理的凝集である可能性が非常に高いので、

こちらも凝集度を上げて結合度を下げる工夫をすべき結合の種類になります。

スタンプ結合

スタンプ結合とはモジュール間で必要以上に大きなデータ構造(クラス・オブジェクトなどなど。。。)をやり取りしてしまう結合のことです。

特徴

  • 必要なデータだけでなく、その周辺情報までまとめて渡してしまう。
  • 呼び出された側が、渡された構造の一部だけを使う。(残りは無視する)
  • 結合度としてはデータ結合より悪く、制御結合よりはマシな中間あたり。
// 大きな構造体
type Property = {
  id: string;
  name: string;
  address: string;
  price: number;
  ownerName: string;
  createdAt: Date;
};

const property: Property = {
  id: "p-001",
  name: "サンプル物件",
  address: "東京都港区...",
  price: 120000,
  ownerName: "山田太郎",
  createdAt: new Date(),
};

// 部屋の価格だけ表示したい関数
const displayPrice = (property: Property) => {
  console.log(`価格: ${property.price}円`);
}

displayPrice(property); // 必要なのはpriceだけなのに余分なデータを渡してしまっている...

上記は価格を表示したいだけなのに大きな構造体を渡してしまっている例です。

displayPriceprice しか使っていないのに、Property 全体を受け取っています。

データ結合

データ結合とはモジュール間で必要最小限のデータだけをやり取りする結合のことです。

特徴

  • やり取りするのは単純なデータ(値や単一変数)
  • 構造体やクラス全体ではなく、本当に必要な情報だけを渡す
  • モジュール間の依存が小さく、再利用しやすい
  • 結合度の低い、良い結合の一つ

[例1]

// 呼び出し側
const property: Property = {
  id: "p-001",
  name: "サンプル物件",
  address: "東京都港区...",
  price: 120000,
  ownerName: "山田太郎",
  createdAt: new Date(),
};

// 上記スタンプ結合の時に上げた例をデータ結合に修正した例
// 部屋の価格だけ表示したい関数(必要な値だけ引数にする)
const displayPrice = (price: number) => {
  console.log(`価格: ${price}円`);
}

// 必要なデータだけ渡す
displayPrice(property.price);

上記のような結合度だと、以下のメリットがあります。

  • 依存度が低い。
  • 再利用しやすい 。
  • テストしやすい。

スタンプ結合の渡すデータを絞ったバージョンと考えてしまって問題ないかなって思います。

メッセージ結合

メッセージ結合とは、モジュール間の結合度の中で最も低いレベルに分類される結合形態です。

モジュール同士がメソッド呼び出し(メッセージ送信)だけでやり取りする形を指します。

特徴

  • 引数が無い関数呼び出しによる依存。
  • 呼び出し元は相手の内部構造やデータ形式を知らない。
  • 内部実装が変わっても、シグネチャ(メソッド名、戻り値の型)が変わらなければ呼び出しているメソッド側は影響を受けない。

[GetCurrentLocationWeather.ts]

type GeolocationPositionType = {
    readonly coords: GeolocationCoordinatesType;
    readonly timestamp: number;
};

type GeolocationCoordinatesType = {
    readonly latitude: number;
    readonly longitude: number;
    readonly altitude: number | null;
    readonly accuracy: number;
    readonly altitudeAccuracy: number | null;
    readonly heading: number | null;
    readonly speed: number | null;
};

/**
 * 位置情報取得関数
 * - Web Geolocation APIを使用して現在地の緯度・経度を取得
 */
const getCurrentPosition = (): Promise<GeolocationPositionType> =>
    new Promise((resolve, reject) => {
        navigator.geolocation.getCurrentPosition(resolve, reject);
    });

/**
 * 現在地の天気取得関数
 * - 位置情報を取得し、天気APIを呼び出して天気データを返す
 * - エラー時はエラーメッセージを返す
 */
const getCurrentLocationWeather = async (): Promise<string> => {
    try {
        const position = await getCurrentPosition();
        const { latitude, longitude } = position.coords;
        const apiUrl = `https://api.example.com/weather?lat=${latitude}&lon=${longitude}`;
        const response = await fetch(apiUrl);
        const data = await response.json();
        return data.weather;
    } catch (error) {
        console.error('Error in getCurrentLocationWeather:', error);
        return 'Failed to get weather';
    }
};

/**
 * 天気情報を取得してメッセージを返す
 * - 晴れ・雨の場合は対応するメッセージを返す
 * - それ以外やエラー時はnullを返す
 */
export const getWeather = async (): Promise<string | null> => {
    const weather = await getCurrentLocationWeather();    
    if (weather.includes('晴')) return '☀️ 良い天気です!';
    if (weather.includes('雨')) return '☔ 傘が必要です!';   
    return null;
};

[Fugafuga.ts]

import { getWeather } from './getWeather'
const weather = await getWeather()

上記に関して言うと

getWeather

  • 責務: 天気情報を取得してユーザー向けメッセージに変換する
  • 結合: getCurrentLocationWeather を呼び出しているが、中身(位置情報の取得やfetchの詳細)は知らない(メッセージ結合)
  • 処理:
    • 天気情報に「晴」が含まれていれば '☀️ 良い天気です!' を返す
    • 天気情報に「雨」が含まれていれば '☔ 傘が必要です!' を返す
    • それ以外(エラー時含む)は null を返す

getCurrentLocationWeather

  • 責務: 現在地の天気情報を取得する
  • 結合: getCurrentPosition を呼び出しているが、内部で navigator.geolocation をどう扱っているかは知らない(メッセージ結合)
  • エラーハンドリング: 位置情報取得エラー・ネットワークエラー等、全ての例外を捕捉し 'Failed to get weather' を返す
  • 処理:
    1. 位置情報を取得
    2. 緯度・経度を使って天気APIを呼び出し
    3. 天気データを返却
    4. エラー時はエラーメッセージを返却(エラーログも出力)

getCurrentPosition

  • 責務: 現在地の位置情報(緯度・経度等)を取得する
  • API: Web標準Geolocation APIの navigator.geolocation.getCurrentPosition を使用
  • 返り値: GeolocationPositionType 型のオブジェクト(緯度・経度・精度・タイムスタンプ等を含む)
  • エラー: 位置情報取得が失敗した場合は Promise が reject される
  • navigator.geolocation.getCurrentPositionはWeb標準のGeolocation APIで現在地を取得します。返り値は以下のような感じになります
const position: GeolocationPosition = {
  coords: {
    latitude: 35.67723,        // 緯度(北緯35度)
    longitude: 139.78572,      // 経度(東経139度)
    altitude: null,            // 高度(通常はnull)
    accuracy: 65,              // 精度(メートル単位)
    altitudeAccuracy: null,    // 高度精度(通常はnull)
    heading: null,             // 進行方向(移動中でなければnull)
    speed: null               // 速度(移動中でなければnull)
  },
  timestamp: 1704067200000    // タイムスタンプ(Unix時間)
};

上記が主な結合度のパターンの解説です。

  • 内容結合(最悪)
  • 共通結合(とても悪い)
  • 外部結合(可能な限り避けるべき)
  • 制御結合(可能な限り避けるべき)
  • スタンプ結合(中程度)
  • データ結合(理想的)
  • メッセージ結合(最も理想的)

下に行けば行くほど、疎結合(結合度低)になり、上に行けば行くほど密結合(結合度高)になります。

凝集度が機能的凝集のみで構成出来ないのと同じように

結合度もメッセージ結合だけでプログラムを構成することは出来無い為、なるべく、結合度を下げる、下げきれないものは小さく保つのが大事になってきます。

改善例

ここでは上記で紹介した密結合を改善していくには?っていう例を示したいと思います。

あくまで一例なのでご留意下さい。。。

内部結合(例1)→データ結合

こちらは上記で示したクラスのprivate変数に直接アクセスしてしまっている内部結合の例です。

[User.ts]

export class User {
  private data: { name: string; email: string; address: string };

  constructor(name: string, email: string, address: string) {
    this.data = { name, email, address };
  }
    
  .
  .
  .
  他の処理
}

[Hoge.ts]

import { User } from './User';

const user = new User("Alice", "alice@example.com", "Tokyo")
// anyとしているので内部メソッドにもアクセスできてしまう。
// 1,インスタンス変数にアクセスしている。
const address = (user as any).data.address;

こちらは以下のようにデータ結合にリファクタが出来ます。

そもそもがUserインスタンスを作る際に必要な値だけを渡しているデータ結合なので、

getterを作ってあげるだけで大丈夫です。

また、readonlyをつけてprivate readonly name: string;みたいな感じにしても良いかもしれませんし、userインタンス作成時に渡す値をUserTypeなどの型に纏めたオブジェクトにして渡しても良いかもしれません。

[User.ts]

export class User {
  private name: string;
  private email: string;
  private address: string;

  constructor(name: string, email: string, address: string) {
    this.name = name;
    this.email = email;
    this.address = address;
  }

  // 必要な情報だけpublicのgetterメソッドで返す
  getAddress(): string {
    return this.address;
  }
}

[Hoge.ts]

import { User } from './User';
const user = new User("Alice", "alice@example.com", "Tokyo");

// privateな変数や内部構造にはアクセスできない

// 必要な情報はpublicメソッド経由で取得
const address = user.getAddress();
console.log(address); // "Tokyo"

内部結合(例2)→データ結合

こちらは上記で示したクラスのprivateメソッドに直接アクセスしてしまっている内部結合の例です。

[Calculator.ts]

export class Calculator {
 
  addDoubled(a: number, b: number) {
    return this.double(a) + this.double(b); 
  }
  
   private double(x: number) { 
   return x * 2; 
 }
}

[Fuga.ts]

import { Calculator } from './Calculator';
const calc = new Calculator();
console.log((calc as any).double(5)); // private を直接呼ぶ

こちらはシンプルな作るなので分かりやすいと思います。純粋にCalculatorクラスですし、2倍にした計算を使いたいならpublicにしてあげれば良いだけですね。

もし仮にprivateメソッドに触りたくなくて、実行結果のみが欲しい場合はそれを実行して値を返す内部結合(例1)のリファクタみたいなgetterメソッドを作る場合もあるかもしれません。

[Calculator.ts]

export class Calculator {
 
  addDoubled(a: number, b: number) {
    return this.double(a) + this.double(b); 
  }
  
  double(x: number) { 
  return x * 2; 
  }
}

[Fuga.ts]

import { Calculator } from './Calculator';
const calc = new Calculator();
console.log(calc.double(5));

外部結合→データ結合

こちらは上記で示した外部結合の結合度を下げるには

[Login.ts]

// ログイン
export const loginUser = async (email: string, password: string) => {
  const response = await fetch('https://api.example.com/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password })
  });

  const data = await response.json();

  if (data.success) {
    localStorage.setItem('token', data.data.access_token);
    // user_infoオブジェクトを、そのまま返す。
    // 外部APIの具体的なレスポンス形式に依存
    return data.data.user_info; 
  }
  throw new Error(data.message);
};

[AuthStatus.ts]

// tokenが有効か確認(tokenが改竄されていないかなどもcheck)
export const checkAuthStatus = async () => {
  const token = localStorage.getItem('token');
  if (!token) return false;

  const response = await fetch('https://api.example.com/auth/status', {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${token}` }
  });

  const data = await response.json();
  // data.valid→tokenが有効化どうか
  return data.success && data.data.valid;
};

[改善例]

以下の様にまず、UserAuth系のロジックを纏めて、重複しているurl部分も定数にしてあげます。

これにより一元管理が可能です。

[UserAuthConst.ts]

export type UserType = {
    id: string;
    email: string;
    name: string;
};

export type LoginApiResponseType = {
    data: {
        access_token: string;
        user_info: {
            id: string;
            email: string;
            full_name: string;
        };
    };
};

export const USER_AUTH_URL = 'https://api.example.com/auth';

[UserAuth.ts]

export class UserAuth {
    async login(email: string, password: string): Promise<UserType> {
        const res = await fetch(`${USER_AUTH_URL}/login`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ email, password }),
        });
        if (!res.ok) throw new Error('login error');
        const data = await res.json();
        if (!isLoginApiResponse(data)) {
            throw new Error('Invalid API response format');
        }
        const user = formatUserFromApiResponse(data)
        localStorage.setItem('token', user.accessToken);
        return user;
    }

    // tokenが有効か確認(tokenが改竄されていないかなどもcheck)
    async checkAuthStatus() {
        const token = localStorage.getItem('token');
        if (!token) return false;

        const res = await fetch(`${USER_AUTH_URL}/status`, {
            method: 'POST',
            headers: { Authorization: `Bearer ${token}` },
        });

        if (!res.ok) throw new Error('check auth error');
        const data = await res.json();
        return data.success && data.data.valid;
    }
    
    private isLoginApiResponse(data: unknown): data is LoginApiResponseType {
        // タイプガード処理
    }
    
    private formatUserFromApiResponse(data: LoginApiResponseType): UserType{
        const userInfo = data.data.user_info;
        const accessToken = data.data.access_token
        return {
            id: userInfo.id,
            email: userInfo.email,
            name: userInfo.full_name,
            accessToken,
        };
};
}

その上でformatUserFromApiResponseの様に外部のapiから来た値を内部のオブジェクトに格納し直せばUserAuth.tsから取得できる値に仮に変更があったとしても呼び出して使っているモジュールAは変更する必要がなくなり、UserAuth.ts内のみの修正で上手くいきそうです。 取得した情報のgetterを作ったりするのも良いかもしれませんね。

private formatUserFromApiResponse(data: LoginApiResponseType): UserType{
    const userInfo = data.data.user_info;
    return {
        id: userInfo.id,
        email: userInfo.email,
        name: userInfo.full_name,
    };

[モジュールA]

const loginUser = await new UserAuth().login('email', 'password')

const funcA() {
    const id = loginUser.id
    // idを使った処理
}

const funcB() {
    const id = loginUser.id
    // idを使った処理
}

上記はUser情報の例なので、自社プロダクトのバックエンドへの通信の例となりますが、

自社で用意しているもの以外の外部APIと通信する場合には尚のこと外部結合の結合度を下げるのは保守性を上げるためには重要になってきそうです。

ブログ作成終盤になって気づいたので、また機会があればそうゆう例も作れたらなって思います。

制御結合(例1)→データ結合

こちらは上記で示した制御結合の結合度を下げるには

[DataFormat.ts]

class DataFormat {
  process(data: string[], mode: string) {
    if (mode === 'csv') {
      // csv format処理...
    } else if (mode === 'json') {
      // json format処理...
    } else if (mode === 'xml') {
      // xml format処理...
    } else if (mode === 'text') {
      // text Format処理...
    }
    throw new Error('Unknown mode');
  }
}

// 呼び出し側が内部の処理を知っている必要がある
const processor = new DataFormat();
const csvResult = processor.process(['a', 'b', 'c'], 'csv');
const jsonResult = processor.process(['a', 'b', 'c'], 'json');

[改善例]

以下のように分割します。そうしたら、単一責任的に出来るし可読性も上がります。

こちらはデータ結合です。

export const csvFormatter = (data: string[]) => {
    // csv format処理...
}
export const jsonFormatter = (data: string[]) => {
    // json format処理...
}

export const textFormatter = (data: string[]) => {
    // text format処理...
}

const csv = csvFormatter(data)
const json = jsonFormatter(data)

ラップで可読性を上げる

コードが似ている場合、抽象化することはよくあると思います。

処理は引数によって振る舞いを変えると思うのですが、引数が増えてくるとパッと見で、その処理がどっちなのか認知し辛かったりする場合があります。

例えばMPAサイトで削除ボタンを押す度にformが送信されてリダイレクトされるサイトがあったとします(画像を上書きすることは出来ず、一回削除する必要があるサイト仕様)

削除の際のnetworkの動きはformのinputタグが持っているdeleteKeyをFormDataにappendしてpostリクエストして更新されたformがgetされるサイト仕様です。

拡張機能で手動操作を模倣した画像の削除を行いたい場合は、以下のようなメソッドになります。

  • getFormHtml
    • ループ内で更新されたformを取得
      • 本来はループでformを何度も取得したくないんですが、サイト仕様自体が削除ボタンを押すと再レンダリングされる仕様です。拡張機能内ではpostリクエストを送っているだけでget処理は走っていないので、本来のサイト仕様と同じ動きなのでループ内でget処理をする事を許容しています。
  • deleteInputName
    • deleteするimageInputを取得
  • formDataにappendしてpostリクエス

[ImageFunc.ts]

/** 画像をリセット(削除)する
 * @remarks サイトがMPAで手動操作でも一回削除リクエストを送る度にリダイレクトされる仕様。
 */
export const resetImages = async (
    imgModalApiUrl: string,
    imageLimit: number,
    deleteKey: 'delete_gaikan' | 'delete_naikan',
    deleteFormVal: '外観削除' | '内観削除'
) => {
    for (let i = 0; i < imageLimit; i++) {
    // formの取得
    const form = await getFormHtml(imgModalApiUrl);
        const deleteInputName = form
            .querySelector(`input[name^="${deleteKey}"]`)
            ?.getAttribute('name');
        if (!deleteInputName) break;
        const formData = new FormData(form);
        formData.append(deleteInputName, deleteFormVal);
        // formをpostリクエスト。リクエスト後サイト仕様で再レンダリングされる
       await postForm(imgModalApiUrl, formData);
    }
};

上記はformDataの微妙な違いやapiへのurlは違えど、画像を削除する処理として重複する部分が多いので抽象化されています。

こちらを呼び出すとしたら呼び出し元はイメージ以下みたいな形になるとします。

こちらはデータ結合です。

[const.ts]

export const APPEARANCE_IMAGE_URL = 'exampleAppearanceUrl'
export const APPEARANCE_IMAGE_LIMIT = 5
export const APPEARANCE_IMAGE_DELETE_KEY = 'delete_gaikan'
export const APPEARANCE_IMAGE_DELETE_VAL = '外観'
export const ROOM_IMAGE_URL = 'exampleRoomUrl'
export const ROOM_IMAGE_LIMIT = 5
export const ROOM_IMAGE_DELETE_KEY = 'delete_naikan'
export const ROOM_IMAGE_DELETE_VAL = '内観'

[AppearanceImage.ts]

export default class AppearanceImage {

    /** 画像を一回リセット(削除)してuploadする */
    async imageUpload(images: Images) {
        await resetImages(
            APPEARANCE_IMAGE_URL, 
            APPEARANCE_IMAGE_LIMIT, 
            APPEARANCE_IMAGE_DELETE_KEY, 
            APPEARANCE_IMAGE_DELETE_VAL
        )
        await this.setAppearanceImages(images)
    }

    private async setAppearanceImages(images: Images) {
        // 画像upload処理...
    }
}

[RoomImage.ts]

class RoomImage {
    /** 画像を一回リセットしてuploadする */
    async imageUpload(images: Images) {

        await resetImages(
            ROOM_IMAGE_URL, 
            ROOM_IMAGE_LIMIT, 
            ROOM_IMAGE_DELETE_KEY, 
            ROOM_IMAGE_DELETE_VAL
        )
        await this.setRoomImages(images)
    }

    private setRoomImages(images: Images) {
        // 画像upload処理...
    }
}

クラスとして分かれているので、上記のコードでいえば問題ないと言えば問題ないかなって思うんですが、

例えば以下のようにラップしてしまえば外観画像の削除の呼び出しをメッセージ結合の様にすることができます。

こうすれば一目で建物と部屋の画像をresetしているって事が分かりやすいかなって思います。

[appearanceImage.ts]

export default class AppearanceImage {
    /** 画像を一回リセットしてuploadする */
    async imageUpload(images: Images) {
            // dataを渡さずにメッセージのみで建物画像のresetImagesを動作できる
        await this.resetAppearanceImages()
        await this.setAppearanceImages(images)
    }

    private async resetAppearanceImages() {
        await resetImages(
            APPEARANCE_IMAGE_URL, 
            APPEARANCE_IMAGE_LIMIT, 
            APPEARANCE_IMAGE_DELETE_KEY, 
            APPEARANCE_IMAGE_DELETE_VAL
        )
    }

    private async setAppearanceImages(images: Images) {
        // 画像upload処理...
    }
}

仮にurlに動的に変わる要素(userId、userToken、uploadされている画像が紐づいている物件Idなど)があり、データ結合でしか呼び出せなかったとしても抽象化した共通処理はラップして、引数は固定出来るところは固定することによって、格段に可読性は上がります。

export default class AppearanceImage {

    /** 画像を一回リセットしてuploadする */
    async imageUpload(images: Images) {
        const url = this.getUrl()
        await this.resetAppearanceImages(url)
        await this.setAppearanceImages(url, images)
    }

    private getUrl() {
        // 動的なurlを取得して返す
    }

    private async resetAppearanceImages(url: string) {
        await resetImages(
            url, 
            APPEARANCE_IMAGE_LIMIT, 
            APPEARANCE_IMAGE_DELETE_KEY, 
            APPEARANCE_IMAGE_DELETE_VAL
        )
    }

    private async setAppearanceImages(url, images) {
        // 画像upload処理...
    }
}

上記例だと、建物と部屋の例なので2通りのパターンで、少し分かりづらいかもですが、

データ結合とはいえ、引数は少なければ少ないほど、認知負荷が下がるかなって思います。

同じ様なデータ形式valueを変えて送るパターンが多い抽象度が高いメソッドであればあるほど、上記のような引数固定でラップする方法は可読性を上げてくれるかなって思います。

まとめ

前回からあわせて、凝集度と結合度について解説しました。

前回同様、コード例を新しく考えるのに苦戦した感じはありますが、

伝わってくれれば嬉しいです。

高凝集・疎結合のコードを意識して書ければ、テストも書きやすいし、後からアサインした人にも読みやすいので保守性が上がりそうです。

最近一部の書籍では凝集度と結合度というのではなく、カプセル化と関心の分離という考え方で書かれたり、改訂されたりしていると聞いたので、そこについても調べて引き続きコードのクオリティを上げる事に努められればなって思います。

いずれにしても凝集度と結合度については古くからある考え方みたいなので知っていて損はないと思うので、もし良かったらこのブログも参考にして頂ければ幸いです。

この記事を読んで興味を持って下さった方がいらっしゃればカジュアルにお話させていただきたく、是非ご応募をお願いします!

iimon採用サイト / Wantedly / Green

参考記事

https://note.com/cyberz_cto/n/n26f535d6c575

https://www.youtube.com/watch?v=-yPPfe13bb0

https://zenn.dev/miya_tech/articles/0dde1228045af6

・Robert C.Martin (2018), Clean Architecture 達人に学ぶソフトウェアの構造と設計, KADOKAWA

https://speakerdeck.com/sonatard/coheision-coupling

https://t-wada.hatenablog.jp/entry/ward-explains-debt-metaphor

・The Clean Architecture, https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

・世界一わかりやすいClean Architecture, https://www.slideshare.net/AtsushiNakamura4/clean-architecture-release

・Code readability, https://speakerdeck.com/munetoshi/code-readability

https://qiita.com/issykatsu/items/2383b3feead6c4e95ccc

https://qiita.com/makotoyc/items/aee473b341cd3a7bd31f

https://developer.mozilla.org/ja/docs/Web/API/GeolocationPosition/coords

https://thoughtbot.com/blog/types-of-coupling?utm_source=chatgpt.com

https://e-words.jp/w/モジュール結合度.html

https://speakerdeck.com/minodriven/ai-good-code-bad-code?slide=7