結合の度合い

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 0

2 が管理者だと、全員が覚えている必要がある。 テスト用カード番号 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. 語彙として。 「ここは結合している」ではなく 「ここは位置の連鎖で、次数が高く、局所性が悪い」と言える。 コードレビューで指摘が具体的になる
  2. 手順として。 強い形を1段弱い形へ書き換える。 位置 → 名前、意味 → 型、値 → 間接層

すべてを弱くしようとしない。 局所性の原則があるので、 モジュールの内側では強い連鎖が自然で、無理に弱めると間接層だけが増える。 できるだけシンプルと衝突する。

見るべきはモジュールの境界をまたぐ連鎖モジュール境界にある分割の代償は、 強い連鎖が境界をまたいだときに顕在化するものでもある。

凝集度について

この文書は結合の側しか扱っていない。

凝集度(モジュールの中身がどれだけ1つの目的に向いているか)の古典的な尺度は Constantine / Yourdon が原典で、オンラインで一次情報が取得できない。 規約を満たせないので書いていない。

実務上の判断はモジュール境界がカバーしている。 「何を一緒に変えることになるか」で切る、という基準は、 凝集度の話を別の言い方でしたものになっている。

関連