- 設計
凝集度と結合度とは:7段階・6段階の一覧とコード例で学ぶ高凝集・疎結合
凝集度とは1つのモジュールが1つの目的に絞られている度合い、結合度とはモジュールが相手の中身をどれだけ知っているかの度合いです。 目指すのは、凝集度がいちばん高い「機能的凝集」と、結合度がいちばん低い「データ結合」です。この組み合わせを「高凝集・疎結合」と呼びます。
この記事では、凝集度7段階と結合度6段階を良い順に並べた早見表を示し、全13段階に Python と TypeScript の短いコード例と実行結果を付けます。後半では、よく見かける3つの形を直す前と後で比べ、レビューで使えるチェックリストにまとめます。ここでいうモジュールは、関数・クラス・ファイルなど、ひとまとまりのコードを指します。
確認環境(確認日 2026-10-05):Ubuntu 24.04、Python 3.11.15、TypeScript 7.0.2(tsc --strict --target es2022 でコンパイル)、Node.js 22.22.2。掲載したコードはすべてこの環境で実行しています。
この記事の結論
- 凝集度は「1つのモジュールが1つの目的に絞られているか」、結合度は「相手の中身をどれだけ知っているか」 です。
- 目指すのは機能的凝集とデータ結合です。
- 見分けるときは、凝集度は「この関数は何をするか、を一言で言えるか」、結合度は「相手を変えたら、こちらも直すことになるか」と問いかけます。
- フラグ引数、グローバルな設定、Utils という名前、内部への直接アクセスが出てきたら、分け直す合図です。
凝集度7段階・結合度6段階の早見表
凝集度(上ほど良い)
| 順 | 名前 | 定義 | 見分け方 | 直し方 |
|---|---|---|---|---|
| 1 | 機能的凝集 | 1つの明確な目的のために全要素が働く | 名前を動詞1つで言い切れる | そのまま保つ |
| 2 | 逐次的凝集 | ある部分の出力が次の部分の入力になる | 「AしてBしてCする」で、値が順に流れる | 段階を別でも使うなら段階ごとに分ける |
| 3 | 通信的凝集 | 同じデータを扱うのでまとめた | 入力は同じだが、戻り値が複数の別の目的 | 目的ごとの関数に分ける |
| 4 | 手続き的凝集 | 実行順序が決まっているのでまとめた | 順番はあるが、行どうしで値の受け渡しが無い | 各手順を関数にし、順番を並べる役だけ残す |
| 5 | 時間的凝集 | 同じ時点に実行するのでまとめた | initialize、cleanup など時点の名前 | 各モジュールに処理を持たせ、呼ぶだけにする |
| 6 | 論理的凝集 | 同じ種類の処理をまとめ、呼ぶ側が選ぶ | 種類を表す引数で分岐している | 種類ごとの関数に分ける |
| 7 | 偶発的凝集 | 理由なく集まっている | Utils、Common などの名前 | 扱う対象ごとのモジュールへ移す |
結合度(上ほど良い)
| 順 | 名前 | 定義 | 見分け方 | 直し方 |
|---|---|---|---|---|
| 1 | データ結合 | 必要な値を引数で受け渡すだけ | 引数が単純な値で、すべて使われている | そのまま保つ |
| 2 | スタンプ結合 | 構造をまるごと渡し、一部だけ使う | 受け取った構造のうち、使う項目が少ない | 使う値だけを渡す |
| 3 | 制御結合 | 何をするかを指示する情報を渡す | フラグやモードの引数で分岐している | 関数を分ける、または値そのものを渡す |
| 4 | 外部結合 | 外部で決まった形式や取り決めを共有する | 列の位置やファイル形式の知識が複数の場所にある | 形式を知る場所を1つにまとめる |
| 5 | 共通結合 | 同じグローバルデータを読み書きする | 引数に無い値で結果が変わる | 値を引数で渡す |
| 6 | 内容結合 | 相手の内部を直接使う・書き換える | 非公開の変数に外から触れている | 公開されたメソッドだけを使う |
凝集度と結合度は、Larry Constantine 氏が 1960 年代後半に構造化設計の一部として考案し、Stevens・Myers・Constantine の 1974 年の発表と、Yourdon・Constantine の 1979 年の書籍で広まった考え方です。段階の数は資料によって異なり、結合度にメッセージ結合などを加えて数える資料もあります。この記事では、凝集度7段階・結合度6段階の分け方で説明します。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「凝集度と結合度とは:7段階・6段階の一覧とコード例で学ぶ高凝集・疎結合」
凝集度7段階のコード例
良い順に並べます。題材は、注文の明細(価格と数量の組)を扱う小さな処理です。各段階とも、Python と TypeScript の実行結果を1つのブロックにまとめています。
凝集度1:機能的凝集(functional)
1つの明確な目的のために、すべての要素が働いている。
Python
def calc_subtotal(items):
"""小計を求める。やることは1つだけ"""
return sum(price * qty for price, qty in items)
print(calc_subtotal([(120, 2), (300, 1)]))
TypeScript
type Item = { price: number; qty: number };
// 小計を求める。やることは1つだけ
function calcSubtotal(items: Item[]): number {
return items.reduce((sum, i) => sum + i.price * i.qty, 0);
}
console.log(calcSubtotal([{ price: 120, qty: 2 }, { price: 300, qty: 1 }]));
# Python
540
# TypeScript
540
目指す形です。 関数名を「〜を求める」「〜を保存する」と動詞1つで言い切れます。
凝集度2:逐次的凝集(sequential)
ある部分の出力が、次の部分の入力になっている。
Python
def build_total_label(items):
subtotal = sum(price * qty for price, qty in items) # 1. 小計を出す
total = subtotal + subtotal * 10 // 100 # 2. 小計から税込みにする
return f"合計 {total:,} 円" # 3. 税込みの値を文字列にする
print(build_total_label([(120, 2), (300, 1)]))
TypeScript
type Item = { price: number; qty: number };
function buildTotalLabel(items: Item[]): string {
const subtotal = items.reduce((sum, i) => sum + i.price * i.qty, 0); // 1. 小計を出す
const total = subtotal + Math.floor((subtotal * 10) / 100); // 2. 小計から税込みにする
return `合計 ${total.toLocaleString("ja-JP")} 円`; // 3. 税込みの値を文字列にする
}
console.log(buildTotalLabel([{ price: 120, qty: 2 }, { price: 300, qty: 1 }]));
# Python
合計 594 円
# TypeScript
合計 594 円
データが一本の流れでつながっているため、まとまりは強めです。途中の段階を別の場所でも使いたくなったら、段階ごとの関数に分けます。
凝集度3:通信的凝集(communicational)
同じデータを扱うという理由でまとまっている。
Python
def summarize_order(items):
"""同じ items を使うが、求めているものは3つとも別の目的"""
subtotal = sum(price * qty for price, qty in items)
count = sum(qty for _, qty in items)
most_expensive = max(price for price, _ in items)
return subtotal, count, most_expensive
print(summarize_order([(120, 2), (300, 1)]))
TypeScript
type Item = { price: number; qty: number };
// 同じ items を使うが、求めているものは3つとも別の目的
function summarizeOrder(items: Item[]) {
const subtotal = items.reduce((sum, i) => sum + i.price * i.qty, 0);
const count = items.reduce((sum, i) => sum + i.qty, 0);
const mostExpensive = Math.max(...items.map((i) => i.price));
return { subtotal, count, mostExpensive };
}
console.log(summarizeOrder([{ price: 120, qty: 2 }, { price: 300, qty: 1 }]));
# Python
(540, 3, 300)
# TypeScript
{ subtotal: 540, count: 3, mostExpensive: 300 }
入力は共通でも、求めているものは別々です。呼ぶ側が3つのうち1つしか使わないなら、目的ごとの関数に分けます。
凝集度4:手続き的凝集(procedural)
決まった実行順序に従うという理由でまとまっている。
Python
def close_counter(log):
"""閉店時の手順。順番は決まっているが、扱うデータは互いに無関係"""
log.append("1. レジの集計を印刷")
log.append("2. 在庫の棚卸し表を保存")
log.append("3. 入口の表示を「準備中」に変更")
log = []
close_counter(log)
print(log)
TypeScript
// 閉店時の手順。順番は決まっているが、扱うデータは互いに無関係
function closeCounter(log: string[]): void {
log.push("1. レジの集計を印刷");
log.push("2. 在庫の棚卸し表を保存");
log.push("3. 入口の表示を「準備中」に変更");
}
const log: string[] = [];
closeCounter(log);
console.log(log);
# Python
['1. レジの集計を印刷', '2. 在庫の棚卸し表を保存', '3. 入口の表示を「準備中」に変更']
# TypeScript
[ '1. レジの集計を印刷', '2. 在庫の棚卸し表を保存', '3. 入口の表示を「準備中」に変更' ]
順番には意味がありますが、各行のあいだでデータの受け渡しがありません。各手順を関数にし、この関数は順番を並べるだけの役にします。
凝集度5:時間的凝集(temporal)
実行される時点が同じという理由でまとまっている。
Python
def initialize(state):
"""起動時に行うことを集めた。順番を入れ替えても動く"""
state["config"] = {"lang": "ja"}
state["cache"] = {}
state["log"] = ["起動しました"]
state = {}
initialize(state)
print(sorted(state))
TypeScript
type State = { config?: { lang: string }; cache?: Map<string, string>; log?: string[] };
// 起動時に行うことを集めた。順番を入れ替えても動く
function initialize(state: State): void {
state.config = { lang: "ja" };
state.cache = new Map();
state.log = ["起動しました"];
}
const state: State = {};
initialize(state);
console.log(Object.keys(state).sort());
# Python
['cache', 'config', 'log']
# TypeScript
[ 'cache', 'config', 'log' ]
「起動時」「終了時」など、時点だけが共通です。設定・キャッシュ・ログそれぞれのモジュールに初期化を持たせ、ここからは呼ぶだけにします。
凝集度6:論理的凝集(logical)
論理的に同じ種類の処理という理由でまとまり、どれを実行するかを呼ぶ側が選ぶ。
Python
import json
def export(rows, kind):
"""「出力する」という分類でまとめ、kind でどれを実行するか選ばせている"""
if kind == "csv":
return "\n".join(",".join(map(str, r)) for r in rows)
if kind == "json":
return json.dumps(rows)
raise ValueError(kind)
print(export([[1, "pen"]], "csv"))
print(export([[1, "pen"]], "json"))
TypeScript
type Row = [number, string];
// 「出力する」という分類でまとめ、kind でどれを実行するか選ばせている
function exportRows(rows: Row[], kind: "csv" | "json"): string {
if (kind === "csv") {
return rows.map((r) => r.join(",")).join("\n");
}
return JSON.stringify(rows);
}
console.log(exportRows([[1, "pen"]], "csv"));
console.log(exportRows([[1, "pen"]], "json"));
# Python
1,pen
[[1, "pen"]]
# TypeScript
1,pen
[[1,"pen"]]
引数(kind)で分岐している点が目印です。export_csv と export_json のように、種類ごとの関数に分けます。
凝集度7:偶発的凝集(coincidental)
理由なく、たまたま1か所に集められている。
Python
class Utils:
"""置き場所に困った関数を集めただけで、互いに関係が無い"""
@staticmethod
def add_tax(price):
return price + price * 10 // 100
@staticmethod
def pad_zero(number, width):
return str(number).zfill(width)
@staticmethod
def is_weekend(weekday):
return weekday in ("土", "日")
print(Utils.add_tax(1000), Utils.pad_zero(7, 3), Utils.is_weekend("土"))
TypeScript
// 置き場所に困った関数を集めただけで、互いに関係が無い
class Utils {
static addTax(price: number): number {
return price + Math.floor((price * 10) / 100);
}
static padZero(n: number, width: number): string {
return String(n).padStart(width, "0");
}
static isWeekend(weekday: string): boolean {
return weekday === "土" || weekday === "日";
}
}
console.log(Utils.addTax(1000), Utils.padZero(7, 3), Utils.isWeekend("土"));
# Python
1100 007 True
# TypeScript
1100 007 true
Utils、Common、Helper といった名前のクラスに現れやすい形です。扱う対象ごとのモジュールに移します(後の「直す前と後」で実行します)。
結合度6段階のコード例
こちらも良い順です。「呼ぶ側」と「呼ばれる側」が互いについて何を知っているかに注目してください。
結合度1:データ結合(data)
必要なデータを引数などで受け渡すだけの関係。
Python
def calc_shipping(weight_kg):
"""必要な値(重さ)だけを引数で受け取る"""
return 500 if weight_kg <= 2 else 800
order = {"id": 1, "customer": "山田", "weight_kg": 1.5}
print(calc_shipping(order["weight_kg"]))
TypeScript
// 必要な値(重さ)だけを引数で受け取る
function calcShipping(weightKg: number): number {
return weightKg <= 2 ? 500 : 800;
}
const order = { id: 1, customer: "山田", weightKg: 1.5 };
console.log(calcShipping(order.weightKg));
# Python
500
# TypeScript
500
目指す形です。 関数は「重さ」しか知らず、注文の形が変わっても影響を受けません。
結合度2:スタンプ結合(stamp)
複合的なデータ構造をまるごと渡し、その一部だけを使う関係。
Python
def calc_shipping(order):
"""注文をまるごと受け取るが、使うのは weight_kg だけ"""
return 500 if order["weight_kg"] <= 2 else 800
order = {"id": 1, "customer": "山田", "weight_kg": 1.5}
print(calc_shipping(order))
TypeScript
type Order = { id: number; customer: string; weightKg: number };
// 注文をまるごと受け取るが、使うのは weightKg だけ
function calcShipping(order: Order): number {
return order.weightKg <= 2 ? 500 : 800;
}
const order: Order = { id: 1, customer: "山田", weightKg: 1.5 };
console.log(calcShipping(order));
# Python
500
# TypeScript
500
関数が注文の構造(キー名やプロパティ名)を知っています。使う項目が1〜2個なら値で渡します。多くの項目を使うなら、まるごと渡すほうが自然な場合もあります。
結合度3:制御結合(control)
何をするかを指示する情報を渡し、相手の処理の流れを制御する関係。
Python
def format_price(price, with_tax):
"""呼ぶ側がフラグで、中の分岐を指示している"""
if with_tax:
return f"{price + price * 10 // 100:,} 円(税込)"
return f"{price:,} 円(税抜)"
print(format_price(1000, True))
print(format_price(1000, False))
TypeScript
// 呼ぶ側がフラグで、中の分岐を指示している
function formatPrice(price: number, withTax: boolean): string {
if (withTax) {
return `${(price + Math.floor((price * 10) / 100)).toLocaleString("ja-JP")} 円(税込)`;
}
return `${price.toLocaleString("ja-JP")} 円(税抜)`;
}
console.log(formatPrice(1000, true));
console.log(formatPrice(1000, false));
# Python
1,100 円(税込)
1,000 円(税抜)
# TypeScript
1,100 円(税込)
1,000 円(税抜)
呼ぶ側が、相手の中にある分岐を知っています。呼び出しの True / False だけでは意味が読み取れない点も目印です。
結合度4:外部結合(external)
外部から決められたデータ形式・通信の取り決め・機器のインターフェースを共有する関係。
Python
def write_line(item_id, name, price):
return f"{item_id},{name},{price}" # 「3列目が価格」という外部の形式
def read_price(line):
return int(line.split(",")[2]) # 同じ形式を、こちらも知っている
line = write_line(1, "pen", 120)
print(line, "->", read_price(line))
TypeScript
function writeLine(itemId: number, name: string, price: number): string {
return `${itemId},${name},${price}`; // 「3列目が価格」という外部の形式
}
function readPrice(line: string): number {
return Number(line.split(",")[2]); // 同じ形式を、こちらも知っている
}
const line = writeLine(1, "pen", 120);
console.log(line, "->", readPrice(line));
# Python
1,pen,120 -> 120
# TypeScript
1,pen,120 -> 120
「3列目が価格」という知識が2か所にあります。形式を知っている場所を1つのモジュールにまとめ、ほかはそのモジュールを通します。
結合度5:共通結合(common)
複数のモジュールが同じグローバルデータを読み書きする関係。
Python
settings = {"discount_rate": 10} # どこからでも書き換えられる共有データ
def start_sale():
settings["discount_rate"] = 20
def calc_price(price):
return price * (100 - settings["discount_rate"]) // 100
print(calc_price(1000))
start_sale()
print(calc_price(1000)) # 同じ呼び出しなのに結果が変わる
TypeScript
const settings = { discountRate: 10 }; // どこからでも書き換えられる共有データ
function startSale(): void {
settings.discountRate = 20;
}
function calcPrice(price: number): number {
return Math.floor((price * (100 - settings.discountRate)) / 100);
}
console.log(calcPrice(1000));
startSale();
console.log(calcPrice(1000)); // 同じ呼び出しなのに結果が変わる
# Python
900
800
# TypeScript
900
800
同じ呼び出しでも、呼ぶ前に何が起きたかで結果が変わります。値を引数で渡す形に直します(後の「直す前と後」で実行します)。
結合度6:内容結合(content)
あるモジュールが、別のモジュールの内部を直接使ったり書き換えたりする関係。
Python
class Cart:
def __init__(self):
self._items = [] # 内部のデータ
def add(self, name, qty):
if qty <= 0:
raise ValueError("数量は1以上")
self._items.append((name, qty))
def count(self):
return sum(qty for _, qty in self._items)
cart = Cart()
cart.add("pen", 2)
cart._items.append(("note", -5)) # 外から内部を直接書き換え、検査を素通りする
print(cart.count())
TypeScript
class Cart {
private items: { name: string; qty: number }[] = []; // 内部のデータ
add(name: string, qty: number): void {
if (qty <= 0) throw new Error("数量は1以上");
this.items.push({ name, qty });
}
count(): number {
return this.items.reduce((sum, i) => sum + i.qty, 0);
}
}
const cart = new Cart();
cart.add("pen", 2);
cart["items"].push({ name: "note", qty: -5 }); // 角かっこで private を素通りする
console.log(cart.count());
# Python
-3
# TypeScript
-3
公開されたメソッド(add)の検査を通らずに、数量 -5 が入りました。公開されたメソッドだけを使い、内部には触れられない書き方にします。
直す前と後:実行結果で比べる
偶発的凝集 → 機能的凝集
直す前は、凝集度7の Utils クラスです。価格の計算・文字列の整形・曜日の判定という、変更の理由が別々の3つが1つのクラスに入っていました。扱う対象ごとにファイルを分けます。
fix1/
├── business_day.py
├── formatting.py
├── main.py
└── pricing.py
pricing.py
"""価格の計算だけを置く"""
def add_tax(price, rate_percent=10):
return price + price * rate_percent // 100
formatting.py
"""表示用の整形だけを置く"""
def pad_zero(number, width):
return str(number).zfill(width)
business_day.py
"""営業日の判定だけを置く"""
def is_weekend(weekday):
return weekday in ("土", "日")
main.py
from business_day import is_weekend
from formatting import pad_zero
from pricing import add_tax
print(add_tax(1000), pad_zero(7, 3), is_weekend("土"))
1100 007 True
結果は直す前と同じ 1100 007 True です。変わったのは、import 文を見れば、そのファイルが何に関わっているかが分かる点です。税率の扱いを変えるときに開くのは pricing.py だけになります。
制御結合 → データ結合
直す前は、結合度3の format_price(price, with_tax) です。呼び出しは format_price(1000, True) となり、True の意味は関数の中を読まないと分かりません。フラグごとに関数を分け、共通の計算だけを小さな関数にします。
def add_tax(price):
return price + price * 10 // 100
def format_price_with_tax(price):
return f"{add_tax(price):,} 円(税込)"
def format_price_without_tax(price):
return f"{price:,} 円(税抜)"
print(format_price_with_tax(1000))
print(format_price_without_tax(1000))
1,100 円(税込)
1,000 円(税抜)
結果は同じで、呼び出しが format_price_with_tax(1000) と名前だけで意味が読める形になりました。関数の中の分岐も無くなっています。TypeScript でも同じ直し方ができます。
function addTax(price: number): number {
return price + Math.floor((price * 10) / 100);
}
function formatPriceWithTax(price: number): string {
return `${addTax(price).toLocaleString("ja-JP")} 円(税込)`;
}
function formatPriceWithoutTax(price: number): string {
return `${price.toLocaleString("ja-JP")} 円(税抜)`;
}
console.log(formatPriceWithTax(1000));
console.log(formatPriceWithoutTax(1000));
1,100 円(税込)
1,000 円(税抜)
共通結合 → データ結合
直す前は、結合度5のコードです。calc_price(1000) という同じ呼び出しが、あいだに start_sale() を挟むと 900 から 800 に変わりました。割引率を引数で受け取る形に直します。
def calc_price(price, discount_rate):
"""割引率を引数で受け取る。結果は引数だけで決まる"""
return price * (100 - discount_rate) // 100
NORMAL_RATE = 10
SALE_RATE = 20
print(calc_price(1000, NORMAL_RATE))
print(calc_price(1000, SALE_RATE))
print(calc_price(1000, NORMAL_RATE)) # 何度呼んでも同じ引数なら同じ結果
900
800
900
結果が引数だけで決まるようになり、3回目の呼び出しは1回目と同じ 900 を返します。テストでも、前準備としてグローバルな設定を書き換える必要が無くなります。
内容結合 → データ結合(TypeScript のプライベートフィールド)
結合度6の TypeScript の例では、private を付けたプロパティに cart["items"] と角かっこでアクセスでき、--strict でもコンパイルが通りました。ドット記法(cart.items)にした場合は、次のエラーになります。
k6_dot.ts(5,6): error TS2341: Property 'items' is private and only accessible within class 'Cart'.
名前を # で始める JavaScript のプライベートフィールドにすると、ドット記法でも角かっこでも、コンパイルの時点で止まります。
class Cart {
#items: { name: string; qty: number }[] = []; // JavaScript のプライベートフィールド
add(name: string, qty: number): void {
if (qty <= 0) throw new Error("数量は1以上");
this.#items.push({ name, qty });
}
count(): number {
return this.#items.reduce((sum, i) => sum + i.qty, 0);
}
}
const cart = new Cart();
cart.add("pen", 2);
cart.#items.push({ name: "note", qty: -5 });
cart["#items"].push({ name: "note", qty: -5 });
npx tsc --noEmit --strict --target es2022 k6_ng.ts
k6_ng.ts(15,6): error TS18013: Property '#items' is not accessible outside class 'Cart' because it has a private identifier.
k6_ng.ts(16,1): error TS7053: Element implicitly has an 'any' type because expression of type '"#items"' can't be used to index type 'Cart'.
Property '#items' does not exist on type 'Cart'.
外からは公開されたメソッドだけを使う形に直すと、数量の検査が必ず通るようになります。
class Cart {
#items: { name: string; qty: number }[] = [];
add(name: string, qty: number): void {
if (qty <= 0) throw new Error("数量は1以上");
this.#items.push({ name, qty });
}
count(): number {
return this.#items.reduce((sum, i) => sum + i.qty, 0);
}
}
const cart = new Cart();
cart.add("pen", 2);
try {
cart.add("note", -5); // 公開されたメソッドを通す
} catch (e) {
console.log("受け付けませんでした:", (e as Error).message);
}
console.log(cart.count());
受け付けませんでした: 数量は1以上
2
Python には、外からのアクセスを言語として止める仕組みがありません。結合度6の例で cart._items に直接追加できたとおりです。先頭にアンダースコアを付けた名前は「内部用」という約束として扱い、レビューで確かめます。
凝集度・結合度と単一責任の原則の違い
どれも「うまく分ける」ための考え方ですが、見ている場所が違います。
| 観点 | 凝集度 | 結合度 | 単一責任の原則 |
|---|---|---|---|
| 見ている場所 | 1つのモジュールの中 | モジュールとモジュールのあいだ | 1つのモジュールと、それを変更したい人・理由 |
| 問い | 中の要素は同じ目的に向かっているか | 相手の中身をどれだけ知っているか | 変更する理由は1つか |
| 表し方 | 7段階の物差し | 6段階の物差し | 守れているかどうかの原則 |
| 使いどころ | 関数やクラスの中身を見直すとき | 引数や呼び出し方を見直すとき | クラスやファイルの切り方を決めるとき |
| 関係 | 機能的凝集に近づけると、変更の理由もまとまりやすい | 結合度を下げると、相手の変更の影響を受けにくい | 「変更の理由」という視点で、凝集の単位を決める手がかりになる |
どれを使って判断するか
| 場面 | 使う考え方 |
|---|---|
| 関数が長い、名前に「〜して〜する」が入る | 凝集度(何段階目かを確かめる) |
| 引数が多い、フラグがある、グローバルを読んでいる | 結合度(何段階目かを確かめる) |
| 1つのクラスに、別々の人から変更の依頼が来る | 単一責任の原則 |
| ファイルやパッケージの切り方を決める | 単一責任の原則で単位を決め、凝集度・結合度で中身と境界を確かめる |
どこでモジュールを分けるか:レビュー用チェックリスト
| No | 確認すること | 当てはまったら | 関係する段階 |
|---|---|---|---|
| 1 | 関数の役割を動詞1つで言い切れるか | 言い切れないなら目的ごとに分ける | 機能的凝集 |
| 2 | 変更の理由が同じものが1か所にまとまっているか | 理由が別のものは別のモジュールへ | 偶発的凝集・単一責任の原則 |
| 3 | Utils、Common、Helper、Manager という名前が無いか | 扱う対象の名前を付けたモジュールへ移す | 偶発的凝集 |
| 4 | 種類やモードを表す引数で分岐していないか | 種類ごとの関数に分ける | 論理的凝集 |
| 5 | 真偽値のフラグ引数が無いか | 関数を分ける、または値そのものを渡す | 制御結合 |
| 6 | initialize や cleanup に、無関係な処理が並んでいないか | 各モジュールに処理を持たせ、呼ぶだけにする | 時間的凝集 |
| 7 | 1つの関数が、目的の違う複数の値を返していないか | 目的ごとの関数に分ける | 通信的凝集 |
| 8 | 受け取ったオブジェクトのうち、使う項目は一部だけではないか | 使う値だけを渡す | スタンプ結合 |
| 9 | 引数に無い値(グローバル変数、共有の設定)で結果が変わらないか | 値を引数で渡す | 共通結合 |
| 10 | ファイル形式や列の位置の知識が、複数の場所に無いか | 形式を知る場所を1つにまとめる | 外部結合 |
| 11 | 相手の非公開の変数に、外から触れていないか | 公開されたメソッドを通す | 内容結合 |
| 12 | 相手の中身を変えたとき、こちらも直すことになるか | なるなら、受け渡す情報を減らす | 結合度全般 |
表計算ソフトに貼れるタブ区切り版です。
No 確認すること 当てはまったら 関係する段階
1 関数の役割を動詞1つで言い切れるか 言い切れないなら目的ごとに分ける 機能的凝集
2 変更の理由が同じものが1か所にまとまっているか 理由が別のものは別のモジュールへ 偶発的凝集・単一責任の原則
3 Utils、Common、Helper、Manager という名前が無いか 扱う対象の名前を付けたモジュールへ移す 偶発的凝集
4 種類やモードを表す引数で分岐していないか 種類ごとの関数に分ける 論理的凝集
5 真偽値のフラグ引数が無いか 関数を分ける、または値そのものを渡す 制御結合
6 initialize や cleanup に、無関係な処理が並んでいないか 各モジュールに処理を持たせ、呼ぶだけにする 時間的凝集
7 1つの関数が、目的の違う複数の値を返していないか 目的ごとの関数に分ける 通信的凝集
8 受け取ったオブジェクトのうち、使う項目は一部だけではないか 使う値だけを渡す スタンプ結合
9 引数に無い値で結果が変わらないか 値を引数で渡す 共通結合
10 ファイル形式や列の位置の知識が、複数の場所に無いか 形式を知る場所を1つにまとめる 外部結合
11 相手の非公開の変数に、外から触れていないか 公開されたメソッドを通す 内容結合
12 相手の中身を変えたとき、こちらも直すことになるか なるなら、受け渡す情報を減らす 結合度全般
段階は「どれだけ直す価値があるか」を話し合うための物差しです。すべてを1段階目にそろえることが目的ではなく、変更が多い場所から順に上の段階へ近づけると、効果が出やすくなります。
確かめて分かったこと
- TypeScript の private は、角かっこのアクセスを止めませんでした。
cart["items"].push(...)は--strictでもコンパイルが通り、合計は -3 になりました。#itemsにすると TS18013 と TS7053 のエラーになります。 - 共通結合のコードは、同じ呼び出しで 900 と 800 を返しました。 エラーにはならず、結果だけが変わるため、気づきにくい動きです。
- 内容結合の例では、数量の検査を通らない -5 がそのまま合計に入りました。 Python でも TypeScript でも結果は同じ -3 です。
- 直したあとも、3つの例とも出力は直す前と同じでした。 凝集度・結合度の改善は動作を変えず、読みやすさと変更のしやすさを変えるものだと確かめられます。
- データ結合とスタンプ結合は、結果がどちらも 500 で、実行しても違いが表れません。 違いが出るのは、注文の構造(キー名)を変えたときに、関数の側も直すことになるかどうかです。
よくある質問
Q. 凝集度と結合度の違いは? A. 凝集度はモジュールの中のまとまり、結合度はモジュールどうしのつながりの強さです。凝集度は高いほど、結合度は低いほど、変更しやすくなります。
Q. 高凝集・疎結合とは? A. 1つのモジュールが1つの目的に絞られ(高凝集)、ほかのモジュールとは必要な値の受け渡しだけでつながっている(疎結合)状態です。段階でいえば、機能的凝集とデータ結合にあたります。
Q. スタンプ結合とデータ結合の違いは? A. 構造をまるごと渡して一部だけ使うのがスタンプ結合、必要な値だけを渡すのがデータ結合です。使う項目が少ないときは値で渡し、多くの項目を使うときは構造ごと渡すほうが読みやすい場合もあります。
Q. 論理的凝集と制御結合の違いは? A. 同じコードを別の側から見たものです。種類を表す引数で分岐する関数の「中」を見ると論理的凝集、その関数と「呼ぶ側」の関係を見ると制御結合です。
Q. 凝集度や結合度を数値で測れますか? A. この記事の段階は、コードを読んで判断する物差しです。まずはチェックリストの項目に当てはまるかどうかを確かめると、レビューで話し合いやすくなります。
まとめ
- 凝集度は「1つの目的に絞られているか」、結合度は「相手の中身をどれだけ知っているか」 です。
- 凝集度は7段階、結合度は6段階で、目指すのは機能的凝集とデータ結合です。
- Utils という名前、種類を表す引数、フラグ引数、グローバルな設定、内部への直接アクセスは、分け直す合図です。
- 直す方法は、目的ごとに分ける、値を引数で渡す、公開されたメソッドを通すの3つに集約できます。
- TypeScript の private は角かっこのアクセスを止めないため、確実に閉じたいときは # で始まるプライベートフィールドを使います。
- 単一責任の原則は「変更の理由」で単位を決める考え方、凝集度・結合度はその中身と境界を確かめる物差しです。
- すべてを1段階目にそろえるのではなく、変更が多い場所から順に近づけます。
参考・出典
- Wikipedia「Cohesion (computer science)」(凝集度の種類と由来): https://en.wikipedia.org/wiki/Cohesion_(computer_science)
- Wikipedia「Coupling (computer programming)」(結合度の種類と由来): https://en.wikipedia.org/wiki/Coupling_(computer_programming)
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「凝集度と結合度とは:7段階・6段階の一覧とコード例で学ぶ高凝集・疎結合」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。