結合の度合い
connascence.io(Creative Commons BY-SA 3.0 で公開されている連鎖度の分類体系)を 取得して読み、理解した内容を自分の言葉で書いたもの。本文の引用はしていない。
この文書は Constantine / Yourdon の結合度・凝集度ではなく、 connascence(連鎖度)の語彙で書いている。
理由は2つ。原典の『Structured Design』がオンラインで読めず、 このリポジトリの規約(一次情報源を実際に取得してから書く)を満たせないこと。 そしてもう一つは、connascence の方が「強い/弱い」の順序と、 弱い形へ書き換える手順が具体的なこと。
モジュール境界がどこで切るかなら、こちらは 切れていないところに、どういう種類の切れなさがあるか。
連鎖度とは
2つの要素が「片方を変えたらもう片方も変えなければならない」関係にあること。
結合を「ある/ない」で見るのではなく、種類に分けて、強さの順に並べるのが要点。 そうすると「結合を減らせ」という言えるだけの助言が、 **「この結合を、1段弱い形に書き換えられるか」**という手を動かせる問いになる。
3つの軸
| 軸 | 何を見るか |
|---|---|
| 強さ(strength) | 見つけにくく、書き換えにくいほど強い |
| 次数(degree) | 影響する要素が2つか、200か |
| 局所性(locality) | 2つの要素がどれだけ近いか(同じ関数・同じクラス・同じモジュール・別のコードベース) |
局所性の原則がこの体系の中心にある。 強い連鎖はモジュールの内側でこそ許され、遠く離れた要素の間には弱い連鎖を使う。
近ければ強い結合はむしろ普通で、同じ関数の中の位置依存は読めば分かる。 同じ結合が、離れた場所にあると壊滅的になる。 だから「強い結合を見つけたら直す」ではなく、「強さと距離の組み合わせを見る」。
静的な形(弱い側)
ソースコードを読めば判定できる。 だから弱い。
連鎖度: 名前
複数の要素が、ある名前について合意していること。
クラス名・メソッド名・引数名を変えたら、参照している側を全部直す。
最も弱く、避けられない。 名前で呼ぶ以上、必ず存在する。 目指すのは「無くす」ではなく「ここまで弱めること」。
連鎖度: 型
複数の要素が、ある要素の型について合意していること。
def calculate_age(birth_day, birth_month, birth_year):呼ぶ側は (1, 9, 1984) なのか (1, 9, 84) なのか ('1', 'September', '1984') なのか
判断できない。型が書かれていないと、合意が暗黙になる。
連鎖度: 意味(規約)
複数の要素が、ある値の意味について合意していること。
if user.is_admin:
return 2
elif user.is_manager:
return 1
else:
return 02 が管理者だと、全員が覚えている必要がある。
テスト用カード番号 9999-9999-9999-9999、「見つからない」と「エラー」の両方を表す None、
通貨の分からない unit_cost = 49.95 も同じ。
弱める手が具体的にある。
マジックな値を名前付き定数にする → 名前へ落ちる
None の代わりに明示的なオブジェクトを返す
Cost(49.95, 'USD') のような型を作る → 型へ落ちる
型にすると、異なる通貨どうしの演算がそもそもできなくなる。
連鎖度: 位置
複数の要素が、値の順序について合意していること。
return ["Thomas", "Richards", 1984, True]
...
if user[3]: # 管理者かどうか順序が変わったら両方直す。引数の順序も同じ。
辞書やオブジェクトに変えると、名前へ落ちる。 これが最も費用対効果の高い書き換え。
連鎖度: アルゴリズム
複数の要素が、特定のアルゴリズムについて合意していること。
送信側と受信側が同じチェックサム方式を使っている
書き込みが encode('utf8')、読み込みが decode('utf8')
メールアドレスの検証が、DB モデル・コントローラ・フロントの3箇所にある
テストが hashlib.md5(user.name).hexdigest() で期待値を計算している
検証の3箇所が食い違うと、利用者は理由の分からないまま登録に失敗する。 言語もファイルも離れているので、次数と局所性の両方で悪い。
テストの例が示しているのは、テストコードもこの結合の当事者になること。 実装が正しく動いていてもテストが落ちる。
動的な形(強い側)
実行時の振る舞いを知らないと判定できない。 だから強い。
連鎖度: 実行順序
複数の要素の実行順序が重要であること。
email.send()
email.setSubject("Hello World") # 送信後なので、良くて何も起きない
ロックの取得と解放の順序も同じ。
離れていれば見つけやすいが、近いと厄介という逆転がここにある。 最後の2行が別のスレッドで呼ばれているなら、追跡は一気に難しくなる。
連鎖度: タイミング
複数の要素の実行の「時刻」が重要であること。
順序ではなく、いつ起きるかに依存している状態。
連鎖度: 値
複数の値が一緒に変わらなければならないこと。
article = Article("Test Contents")
assert article.state == ArticleState.Draft # 初期状態を直接書いている初期状態を変えたらテストが落ちる。
間接の層を1つ挟むと解ける。
class ArticleState(Enum):
Draft = 1
Published = 2
InitialState = Draft実装もテストも InitialState を見るようにすれば、
状態機械を変えてもテストは落ちない。
連鎖度: 同一性
複数の要素が、同一の実体を参照していなければならないこと。
等しい値ではなく、同じインスタンスであることに依存している状態。最も強い。
使い方
分類を暗記することが目的ではない。 使い道は2つ。
- 語彙として。 「ここは結合している」ではなく 「ここは位置の連鎖で、次数が高く、局所性が悪い」と言える。 コードレビューで指摘が具体的になる
- 手順として。 強い形を1段弱い形へ書き換える。 位置 → 名前、意味 → 型、値 → 間接層
すべてを弱くしようとしない。 局所性の原則があるので、 モジュールの内側では強い連鎖が自然で、無理に弱めると間接層だけが増える。 できるだけシンプルと衝突する。
見るべきはモジュールの境界をまたぐ連鎖。 モジュール境界にある分割の代償は、 強い連鎖が境界をまたいだときに顕在化するものでもある。
凝集度について
この文書は結合の側しか扱っていない。
凝集度(モジュールの中身がどれだけ1つの目的に向いているか)の古典的な尺度は Constantine / Yourdon が原典で、オンラインで一次情報が取得できない。 規約を満たせないので書いていない。
実務上の判断はモジュール境界がカバーしている。 「何を一緒に変えることになるか」で切る、という基準は、 凝集度の話を別の言い方でしたものになっている。
関連
- モジュール境界 — どこで切るか。切った後に何が起きるか
- レイヤリングとドメインモデル — 切った中をどう積むか
- 設計 — 弱めすぎると間接層が増える
- デザインパターンの使いどころ — 語彙として使うという同じ発想
- コード — 継承は強い連鎖を作りやすい
- コードレビュー — 指摘の語彙として使う
- テスト — テストコードも連鎖の当事者になる