以下の内容はhttps://syu-m-5151.hatenablog.com/entry/2025/10/31/125256より取得しました。


近すぎず、遠すぎず - コードの結合度とちょうどいい距離の測り方

はじめに

人間関係が数値化できればなぁって思ったこともありますか?僕はあります。「この人とは、ちょうどいい距離感だな」とか、「もうちょっと親しくなりたいけど、近づきすぎると息苦しいかもしれない」とか。そういう、言葉にしづらい感覚を、もし数字で表せたら——なんて。

コードを書いているとき、似たようなことを考える。

「このモジュールとあのモジュール、近すぎるな」と思う瞬間がある。あるいは逆に、「これ、もっと近くにあった方がいいんじゃないか」と。モジュール同士の距離感。誰もが一度は悩んだことがあるはずだ。近すぎても、遠すぎてもいけない。でも、「ちょうどいい」って、どういうことだろう

バグを修正したときのことだ。直接的には関係ないはずの、別の場所が壊れる。そんな経験、ないだろうか。それは、見えない糸が張り巡らされている証拠だ。データの持ち方への暗黙の依存。前提条件。実行順序。そういう、目に見えにくい結合が、コードのあちこちに潜んでいる。

大切なのは、結合をゼロにすることじゃない。そんなことは、できない。むしろ、適切にバランスさせることだ。人間関係と同じように。

Vlad Khononov が書いた「Balancing Coupling in Software Design」という本がある。この本は、結合度を測るための、実践的なフレームワークを提供している。結合の強さ、距離、そして変更の頻度——この3つの軸。これで測る。バランスを評価する。

この本が教えてくれることがある。機能は線形に増える。でも、複雑さは指数関数的に膨れ上がる。そして、僕たち人間には認知的な限界がある。だから、複雑さに対処するには、システムの形を変えるしかない。そのための道具が、結合なのだ。

この記事では、その概念を Rust プロジェクトに適用してみる。実際に測定して、分析できるツールを作る。コードの「ちょうどいい距離感」を数値化し、可視化する。そんな試みである。

Balancing Couplingの核心概念

3つの次元で結合度を測る

すごく端的に話すとKhononov のフレームワークは、結合度を 3 つの軸で評価する。

1. Integration Strength(統合強度)

コンポーネント間で共有される知識の量。次の 4 つのレベルに分類される。

  • Intrusive Coupling(侵入的結合)- 内部実装の詳細に依存
  • Functional Coupling(機能的結合)- 共有された責任による依存
  • Model Coupling(モデル結合)- ビジネスドメインモデルの共有
  • Contract Coupling(契約結合)- インターフェース/トレイトによる抽象化

下に行くほど結合が弱く、望ましい。

2. Distance(距離)

依存関係がどれだけ離れているかを測る。

  • 同じ関数内
  • 同じモジュール内
  • 異なるモジュール
  • 異なるクレート
  • 異なるサービス(マイクロサービスの場合)

距離が遠いほど、変更のコストが高くなる。

3. Volatility(変動性)

コンポーネント変更頻度を示す。

バランスの公式

これらを組み合わせた「バランスの方程式」は、概念的には次のように表現できる。

BALANCE = (STRENGTH XOR DISTANCE) OR NOT VOLATILITY

概念の解釈

MODULARITY = STRENGTH XOR DISTANCE

  • 強い結合なら距離を近く(局所性を保つ)
  • 弱い結合なら距離を遠くても良い(疎結合
  • この 2 つのパターンが理想的

BALANCE = MODULARITY OR NOT VOLATILITY

  • モジュラーである、または変動性が低い(安定している)
  • どちらかの条件を満たせばバランスが取れている

数値計算への変換

実装では、論理演算を数値計算に変換する。

  • XOR - 両極端(強×近、弱×遠)の和として計算
  • OR - 最大値(max)として計算
  • NOT - 補数(1.0 - x)として計算

ここで押さえておきたいのは、結合をゼロにするのが目的ではないということ。適切にバランスさせることが肝心で、コンテキストに応じて最適な形を選ぶ必要がある。

Connascence(共依存性)

結合度をさらに細かく分析するには、Meilir Page-Jones が提唱した「Connascence」の概念が役立つ。

Static Connascence(静的共依存性)

コンパイル時に検出可能なもの。

  • Connascence of Name(CoN)- 名前への依存
  • Connascence of Type(CoT)- 型への依存
  • Connascence of Meaning(CoM)- 値の意味への依存
  • Connascence of Position(CoP)- パラメータ順序への依存
  • Connascence of Algorithm(CoA)- アルゴリズムへの依存

Dynamic Connascence(動的共依存性)

実行時に検出されるもの。

  • Connascence of Execution(CoE)- 実行順序への依存
  • Connascence of Timing(CoT)- タイミングへの依存
  • Connascence of ValueCoV)- 値の同期的変更への依存
  • Connascence of Identity(CoI)- 同一インスタンスへの依存

動的共依存性は、最も弱いものでも、最も強い静的共依存性よりも強い結合を意味する。

Rustにおける既存ツール

1. cargo-modules

モジュール構造と依存関係を可視化できる。

https://crates.io/crates/cargo-modulescrates.io

インストール:

cargo install cargo-modules

使い方:

# モジュール構造をツリー表示
cargo modules structure

# モジュール間の依存関係をグラフ表示
cargo modules dependencies

# 循環依存の検出
cargo modules dependencies --acyclic

# 孤立したファイルの検出
cargo modules orphans

特徴:

  • モジュール階層の視覚化
  • 循環依存の検出(リファクタリングの重要な手がかり)
  • 未使用ファイルの発見
  • GraphViz と連携可能

2. rust-code-analysis

Mozilla が開発した、多言語対応のコードメトリクス計測ツール。

github.com

インストール:

cargo install rust-code-analysis-cli

使い方:

# 単一ファイルの分析
rust-code-analysis-cli --metrics -p src/main.rs

# プロジェクト全体の分析
rust-code-analysis-cli --metrics -p ./src

# JSON形式で出力
rust-code-analysis-cli --metrics -O json -o metrics.json -p ./src

計測可能なメトリクス

  • CC (Cyclomatic Complexity) - 循環的複雑度
  • COGNITIVE - 認知的複雑度
  • HALSTEAD - Halstead メトリクス(Bugs, Difficulty, Effort, Volume 等)
  • LOC 系 - SLOC、PLOC、LLOC 等
  • NOM - メソッド数
  • NARGS - 引数の数
  • NEXITS - 出口の数
  • WMC - クラスごとの循環的複雑度の合計

Rust コードの複雑度を他言語と比較する研究でも使用されており、信頼性が高い。

3. cargo tree

Cargo 組み込みの依存関係ツリー表示コマンド。

doc.rust-lang.org

使い方:

# 依存関係ツリーの表示
cargo tree

# 深さを制限
cargo tree --depth 1

# 特定のパッケージを除外
cargo tree --prune serde

# 重複する依存関係を表示
cargo tree --duplicates

# リバース依存関係(何がこのクレートに依存しているか)
cargo tree --invert <package-name>

4. その他の有用なツール

Balanced Couplingを測定するカスタムツールの実装

既存ツールは有用だが、Khononov のモデルを直接適用するには限界がある。特に、Integration Strength の分類、Dynamic Connascence の検出、Git 履歴との連携、Balance Score の計算といった機能が不足している。

これらを測定するため、カスタムツールを実装していこう。

基本的なアプローチ

必要な依存関係を Cargo.toml に追加します。

[dependencies]
syn = { version = "2.0", features = ["full", "visit"] }
quote = "1.0"
walkdir = "2.4"
thiserror = "2.0"

実装例です。

use syn::{visit::Visit, ItemFn, ItemImpl};

/// 結合度メトリクスを保持する構造体
///
/// Integration Strength、Distance、Connascenceの3つの次元を測定
#[derive(Debug, Default, Clone)]
pub struct CouplingMetrics {
    // Integration Strength
    pub intrusive_count: usize,
    pub functional_count: usize,
    pub model_count: usize,
    pub contract_count: usize,

    // Distance
    pub same_module: usize,
    pub cross_module: usize,
    pub cross_crate: usize,

    // Connascence
    pub name_coupling: Vec<String>,
    pub type_coupling: Vec<String>,
    pub position_coupling: Vec<String>,
}

/// ASTを訪問して結合度を分析するアナライザー
#[derive(Debug)]
pub struct CouplingAnalyzer {
    pub metrics: CouplingMetrics,
    pub current_module: String,
}

impl CouplingAnalyzer {
    /// 新しいアナライザーを作成
    ///
    /// # Arguments
    /// * `module_name` - 分析対象のモジュール名
    fn new(module_name: String) -> Self {
        Self {
            metrics: CouplingMetrics::default(),
            current_module: module_name,
        }
    }

    /// 関数シグネチャを分析してConnascence of Positionを検出
    ///
    /// # Arguments
    /// * `sig` - 分析対象の関数シグネチャ
    fn analyze_function_signature(&mut self, sig: &syn::Signature) {
        // 引数の数をチェック(Connascence of Position)
        if sig.inputs.len() > 3 {
            self.metrics.position_coupling.push(
                format!("Function {} has {} parameters",
                    sig.ident, sig.inputs.len())
            );
        }
    }

    /// 2つのモジュールパス間の距離を計算
    ///
    /// # Arguments
    /// * `from_path` - 開始パス (例: "crate::module::submodule")
    /// * `to_path` - 終了パス
    ///
    /// # Returns
    /// モジュール階層における段数(距離)
    fn calculate_distance(&self, from_path: &str, to_path: &str) -> usize {
        let from_parts: Vec<&str> = from_path.split("::").collect();
        let to_parts: Vec<&str> = to_path.split("::").collect();

        // 共通の祖先を見つける
        let common = from_parts.iter()
            .zip(to_parts.iter())
            .take_while(|(a, b)| a == b)
            .count();

        (from_parts.len() - common) + (to_parts.len() - common)
    }
}

impl<'ast> Visit<'ast> for CouplingAnalyzer {
    /// 関数定義を訪問
    fn visit_item_fn(&mut self, node: &'ast ItemFn) {
        // 関数定義を分析
        self.analyze_function_signature(&node.sig);
        syn::visit::visit_item_fn(self, node);
    }

    /// implブロックを訪問してContract/Intrusive Couplingを検出
    fn visit_item_impl(&mut self, node: &'ast ItemImpl) {
        // トレイト実装を分析(Contract Coupling)
        if node.trait_.is_some() {
            self.metrics.contract_count += 1;
        } else {
            // 具象型への直接実装(Intrusive Coupling)
            self.metrics.intrusive_count += 1;
        }
        syn::visit::visit_item_impl(self, node);
    }
}

Volatility(変動性)の測定

Git の履歴から変更頻度を分析します。

use std::process::Command;
use std::collections::HashMap;
use thiserror::Error;

/// Volatility分析のエラー型
#[derive(Error, Debug)]
pub enum VolatilityError {
    #[error("Git command failed: {0}")]
    GitCommandFailed(String),
    #[error("Failed to parse Git output: {0}")]
    ParseError(String),
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    #[error("UTF-8 conversion error: {0}")]
    Utf8Error(#[from] std::string::FromUtf8Error),
}

/// ファイルの変更頻度を分析するアナライザー
///
/// Git履歴を解析して各ファイルの変動性を評価
#[derive(Debug, Default, Clone)]
pub struct VolatilityAnalyzer {
    file_changes: HashMap<String, usize>,
}

/// 変動性のレベル
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum VolatilityLevel {
    /// 低変動性(汎用サブドメイン)
    Low,
    /// 中変動性(サポートサブドメイン)
    Medium,
    /// 高変動性(コアサブドメイン)
    High,
}

impl VolatilityAnalyzer {
    /// 新しいアナライザーを作成
    fn new() -> Self {
        Self::default()
    }

    /// Git履歴を解析して変更頻度を収集
    ///
    /// # Arguments
    /// * `months` - 過去何ヶ月分の履歴を分析するか
    ///
    /// # Returns
    /// 成功時は `Ok(())`、Git コマンド実行失敗時はエラー
    ///
    /// # Errors
    /// - Git コマンドが失敗した場合
    /// - 出力のパースに失敗した場合
    ///
    /// # Example
    /// ```
    /// let mut analyzer = VolatilityAnalyzer::new();
    /// analyzer.analyze_git_history(6)?; // 過去6ヶ月を分析
    /// ```
    pub fn analyze_git_history(&mut self, months: usize) -> Result<(), VolatilityError> {
        let output = Command::new("git")
            .args([
                "log",
                "--pretty=format:",
                "--name-only",
                &format!("--since={} months ago", months)
            ])
            .output()?;

        // Git コマンドが失敗した場合のチェック
        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(VolatilityError::GitCommandFailed(stderr.to_string()));
        }

        let files = String::from_utf8(output.stdout)?;
        for file in files.lines().filter(|l| !l.is_empty()) {
            *self.file_changes.entry(file.to_string())
                .or_insert(0) += 1;
        }

        Ok(())
    }

    /// ファイルの変動性レベルを分類
    ///
    /// # Arguments
    /// * `file` - 分類対象のファイルパス
    ///
    /// # Returns
    /// 変動性レベル(Low/Medium/High)
    fn classify_volatility(&self, file: &str) -> VolatilityLevel {
        let changes = self.file_changes.get(file).copied().unwrap_or(0);

        match changes {
            0..=2 => VolatilityLevel::Low,
            3..=10 => VolatilityLevel::Medium,
            _ => VolatilityLevel::High,
        }
    }

    /// 最も変更頻度の高いファイルを取得
    ///
    /// # Arguments
    /// * `n` - 取得する上位ファイル数
    ///
    /// # Returns
    /// (ファイルパス, 変更回数) のタプルのベクター
    fn top_volatile_files(&self, n: usize) -> Vec<(&str, usize)> {
        let mut files: Vec<_> = self.file_changes
            .iter()
            .map(|(file, &count)| (file.as_str(), count))
            .collect();

        files.sort_by(|a, b| b.1.cmp(&a.1));
        files.truncate(n);
        files
    }
}

バランススコアの計算

/// Balancing Couplingのスコアを計算
///
/// Khononovのモデルに基づき、Strength、Distance、Volatilityの
/// 3次元からバランススコアを算出
#[derive(Debug, Clone)]
struct BalancedCouplingScore {
    /// 結合の強さ: 0.0 (弱い) から 1.0 (強い)
    strength: f64,
    /// 距離: 0.0 (近い) から 1.0 (遠い)
    distance: f64,
    /// 変動性: 0.0 (安定) から 1.0 (頻繁に変更)
    volatility: f64,
}

impl BalancedCouplingScore {
    /// 新しいスコアを作成
    ///
    /// # Arguments
    /// * `strength` - 結合の強さ (0.0-1.0)
    /// * `distance` - 距離 (0.0-1.0)
    /// * `volatility` - 変動性 (0.0-1.0)
    ///
    /// # Panics
    /// 各値が 0.0-1.0 の範囲外の場合にパニック
    fn new(strength: f64, distance: f64, volatility: f64) -> Self {
        assert!((0.0..=1.0).contains(&strength), "strength must be 0.0-1.0");
        assert!((0.0..=1.0).contains(&distance), "distance must be 0.0-1.0");
        assert!((0.0..=1.0).contains(&volatility), "volatility must be 0.0-1.0");

        Self {
            strength,
            distance,
            volatility,
        }
    }

    /// モジュラリティを計算
    ///
    /// # Formula
    /// MODULARITY = STRENGTH XOR DISTANCE (論理的な意味での排他的論理和)
    ///
    /// 理想的な状態:
    /// - 強い結合なら距離が近い (strength が高く distance が低い)
    /// - 弱い結合なら距離が遠くても良い (strength が低く distance が高い)
    ///
    /// # Returns
    /// モジュラリティスコア (0.0-1.0、高いほど良い)
    fn calculate_modularity(&self) -> f64 {
        // 強い結合 × 近い距離 = 良い(局所性が高い)
        let ideal_close = self.strength * (1.0 - self.distance);
        // 弱い結合 × 遠い距離 = 良い(疎結合が保たれている)
        let ideal_far = (1.0 - self.strength) * self.distance;

        ideal_close + ideal_far
    }

    /// バランススコアを計算
    ///
    /// # Formula
    /// BALANCE = MODULARITY OR (NOT VOLATILITY) (論理的な意味での論理和)
    ///
    /// バランスが取れている状態:
    /// - モジュラーである、または
    /// - 変動性が低い(安定している)
    ///
    /// # Returns
    /// バランススコア (0.0-1.0、高いほど良い)
    fn calculate_balance(&self) -> f64 {
        let modularity = self.calculate_modularity();
        let stability = 1.0 - self.volatility;

        // 論理和の近似:max を使用
        modularity.max(stability)
    }

    /// 結合の問題点を識別
    ///
    /// # Returns
    /// 検出された問題のリスト
    fn identify_issues(&self) -> Vec<CouplingIssue> {
        let mut issues = Vec::new();

        // パターン1: グローバル複雑性
        // 強い結合 + 遠い距離 = 変更の調整コストが高い
        if self.strength > 0.7 && self.distance > 0.7 {
            issues.push(CouplingIssue {
                severity: IssueSeverity::High,
                description: "Strong coupling over long distance increases global complexity"
                    .to_string(),
                recommendation: "Consider moving coupled components closer or reducing coupling strength"
                    .to_string(),
            });
        }

        // パターン2: ローカル複雑性
        // 弱い結合 + 近い距離 = 不要な抽象化の可能性
        if self.strength < 0.3 && self.distance < 0.3 {
            issues.push(CouplingIssue {
                severity: IssueSeverity::Medium,
                description: "Weak coupling at close distance may indicate unnecessary indirection"
                    .to_string(),
                recommendation: "Consider consolidating components or increasing distance"
                    .to_string(),
            });
        }

        // パターン3: カスケード変更リスク
        // 強い結合 + 高変動性 = 変更が広範囲に波及
        if self.strength > 0.7 && self.volatility > 0.7 {
            issues.push(CouplingIssue {
                severity: IssueSeverity::Critical,
                description: "Strong coupling with volatile component creates cascading change risk"
                    .to_string(),
                recommendation: "Isolate volatile components or reduce coupling strength"
                    .to_string(),
            });
        }

        // パターン4: 低モジュラリティ
        let modularity = self.calculate_modularity();
        if modularity < 0.4 {
            issues.push(CouplingIssue {
                severity: IssueSeverity::Medium,
                description: format!("Low modularity score: {:.2}", modularity),
                recommendation: "Review coupling strength and distance relationship"
                    .to_string(),
            });
        }

        issues
    }
}

/// 結合に関する問題
#[derive(Debug, Clone)]
struct CouplingIssue {
    severity: IssueSeverity,
    description: String,
    recommendation: String,
}

/// 問題の重要度
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum IssueSeverity {
    Low,
    Medium,
    High,
    Critical,
}

実践的な使用例

プロジェクト全体の分析

必要な依存関係を追加します。

[dependencies]
syn = { version = "2.0", features = ["full", "visit"] }
walkdir = "2.4"

実装例です。

use walkdir::WalkDir;
use std::path::Path;
use std::ffi::OsStr;
use std::fs;

/// プロジェクト全体のメトリクスを保持
#[derive(Debug, Default)]
struct ProjectMetrics {
    measurements: Vec<CouplingMeasurement>,
    file_count: usize,
    module_count: usize,
}

/// 結合度の測定結果
#[derive(Debug)]
struct CouplingMeasurement {
    from_module: String,
    to_module: String,
    strength: f64,
    distance: f64,
    volatility: f64,
}

/// プロジェクト全体を分析
///
/// # Arguments
/// * `project_path` - プロジェクトのルートパス
///
/// # Returns
/// プロジェクトメトリクス、またはエラー
///
/// # Example
/// ```
/// let metrics = analyze_project("./src")?;
/// println!("Total files analyzed: {}", metrics.file_count);
/// ```
fn analyze_project(project_path: &str) -> Result<ProjectMetrics, Box<dyn std::error::Error>> {
    let mut project_metrics = ProjectMetrics::default();

    // 1. 構造的な依存関係を分析
    for entry in WalkDir::new(project_path)
        .follow_links(true)
        .into_iter()
        .filter_map(|e| e.ok())
    {
        let path = entry.path();

        // Rustファイルのみを処理
        if path.extension() == Some(OsStr::new("rs")) {
            analyze_rust_file(path, &mut project_metrics)?;
        }
    }

    // 2. Git履歴から変動性を分析
    let mut volatility = VolatilityAnalyzer::new();
    volatility.analyze_git_history(6)?;

    // 3. バランススコアを計算
    calculate_balance_scores(&mut project_metrics, &volatility);

    Ok(project_metrics)
}

/// 個別のRustファイルを分析
///
/// # Arguments
/// * `path` - ファイルパス
/// * `project_metrics` - プロジェクトメトリクスの可変参照
fn analyze_rust_file(
    path: &Path,
    project_metrics: &mut ProjectMetrics,
) -> Result<(), Box<dyn std::error::Error>> {
    let content = fs::read_to_string(path)?;
    let syntax = syn::parse_file(&content)?;

    let module_name = path
        .to_string_lossy()
        .replace('/', "::")
        .replace(".rs", "");

    let mut analyzer = CouplingAnalyzer::new(module_name);
    analyzer.visit_file(&syntax);

    project_metrics.file_count += 1;
    project_metrics.module_count += 1;

    Ok(())
}

/// バランススコアを計算してプロジェクトメトリクスに追加
///
/// # Arguments
/// * `metrics` - プロジェクトメトリクスの可変参照
/// * `volatility` - 変動性アナライザーの参照
fn calculate_balance_scores(
    metrics: &mut ProjectMetrics,
    volatility: &VolatilityAnalyzer,
) {
    for measurement in &metrics.measurements {
        let score = BalancedCouplingScore::new(
            measurement.strength,
            measurement.distance,
            measurement.volatility,
        );

        let issues = score.identify_issues();
        if !issues.is_empty() {
            eprintln!(
                "Issues found in coupling from {} to {}:",
                measurement.from_module, measurement.to_module
            );
            for issue in issues {
                eprintln!("  [{:?}] {}", issue.severity, issue.description);
            }
        }
    }
}

レポート生成

use std::io::{self, Write};

/// Markdownフォーマットでレポートを生成
///
/// # Arguments
/// * `metrics` - プロジェクトメトリクス
/// * `writer` - 出力先 (stdout、ファイルなど)
///
/// # Example
/// ```
/// let metrics = analyze_project("./src")?;
/// generate_report(&metrics, &mut std::io::stdout())?;
/// ```
fn generate_report<W: Write>(
    metrics: &ProjectMetrics,
    writer: &mut W,
) -> io::Result<()> {
    writeln!(writer, "# Coupling Analysis Report\n")?;

    // サマリーセクション
    write_summary(metrics, writer)?;

    // 統合強度の分布
    write_strength_distribution(metrics, writer)?;

    // 問題の検出
    write_issues(metrics, writer)?;

    // 変動性分析
    write_volatility_analysis(metrics, writer)?;

    Ok(())
}

/// サマリーセクションを出力
fn write_summary<W: Write>(
    metrics: &ProjectMetrics,
    writer: &mut W,
) -> io::Result<()> {
    writeln!(writer, "## Summary\n")?;
    writeln!(writer, "- **Total Files**: {}", metrics.file_count)?;
    writeln!(writer, "- **Total Modules**: {}", metrics.module_count)?;
    writeln!(
        writer,
        "- **Total Couplings**: {}\n",
        metrics.measurements.len()
    )?;
    Ok(())
}

/// Integration Strengthの分布を出力
fn write_strength_distribution<W: Write>(
    metrics: &ProjectMetrics,
    writer: &mut W,
) -> io::Result<()> {
    writeln!(writer, "## Integration Strength Distribution\n")?;

    let total = metrics.measurements.len() as f64;
    let contract = metrics
        .measurements
        .iter()
        .filter(|m| m.strength <= 0.25)
        .count();
    let model = metrics
        .measurements
        .iter()
        .filter(|m| m.strength > 0.25 && m.strength <= 0.50)
        .count();
    let functional = metrics
        .measurements
        .iter()
        .filter(|m| m.strength > 0.50 && m.strength <= 0.75)
        .count();
    let intrusive = metrics
        .measurements
        .iter()
        .filter(|m| m.strength > 0.75)
        .count();

    writeln!(
        writer,
        "- **Contract Coupling** (weakest): {} ({:.1}%)",
        contract,
        (contract as f64 / total) * 100.0
    )?;
    writeln!(
        writer,
        "- **Model Coupling**: {} ({:.1}%)",
        model,
        (model as f64 / total) * 100.0
    )?;
    writeln!(
        writer,
        "- **Functional Coupling**: {} ({:.1}%)",
        functional,
        (functional as f64 / total) * 100.0
    )?;
    writeln!(
        writer,
        "- **Intrusive Coupling** (strongest): {} ({:.1}%)\n",
        intrusive,
        (intrusive as f64 / total) * 100.0
    )?;

    Ok(())
}

/// 検出された問題を出力
fn write_issues<W: Write>(
    metrics: &ProjectMetrics,
    writer: &mut W,
) -> io::Result<()> {
    writeln!(writer, "## Detected Issues\n")?;

    let mut has_issues = false;

    for measurement in &metrics.measurements {
        let score = BalancedCouplingScore::new(
            measurement.strength,
            measurement.distance,
            measurement.volatility,
        );

        let issues = score.identify_issues();
        if !issues.is_empty() {
            has_issues = true;
            writeln!(
                writer,
                "### {} → {}\n",
                measurement.from_module, measurement.to_module
            )?;

            for issue in issues {
                writeln!(writer, "**{:?}**: {}", issue.severity, issue.description)?;
                writeln!(writer, "- *Recommendation*: {}\n", issue.recommendation)?;
            }
        }
    }

    if !has_issues {
        writeln!(writer, "No significant coupling issues detected.\n")?;
    }

    Ok(())
}

/// 変動性分析を出力
fn write_volatility_analysis<W: Write>(
    metrics: &ProjectMetrics,
    writer: &mut W,
) -> io::Result<()> {
    writeln!(writer, "## High Volatility Analysis\n")?;

    // 高変動性のファイルを抽出
    let high_volatility: Vec<_> = metrics
        .measurements
        .iter()
        .filter(|m| m.volatility > 0.7)
        .collect();

    if high_volatility.is_empty() {
        writeln!(writer, "No high volatility modules detected.\n")?;
    } else {
        writeln!(writer, "Modules with high change frequency:\n")?;
        for measurement in high_volatility {
            writeln!(
                writer,
                "- `{}` (volatility: {:.2})",
                measurement.from_module, measurement.volatility
            )?;
        }
        writeln!(writer)?;
    }

    Ok(())
}

実践的なガイドライン

結合度を改善するためのパターン

1. 強い結合が必要な場合は距離を近くする

頻繁に一緒に変更される機能は、同じモジュール内に配置するべきだ。

// Good: 密接に関連する機能を同じモジュールに配置
mod user_profile {
    pub struct User { /* ... */ }
    pub struct UserProfile { /* ... */ }

    // Userと常に一緒に使われる
    impl User {
        pub fn get_profile(&self) -> &UserProfile { /* ... */ }
    }
}

DDD 的に言えば、同じ Bounded Context 内の概念は同じモジュールに配置する。

2. 弱い結合なら距離を遠くしても良い

インターフェース(trait)を通じた疎結合なら、別クレートに分けても問題ない。

// core/src/lib.rs - インターフェース定義
pub trait NotificationService {
    fn send(&self, message: &str) -> Result<(), Error>;
}

// adapters/email/src/lib.rs - 実装は別クレート
use core::NotificationService;

pub struct EmailService;
impl NotificationService for EmailService {
    fn send(&self, message: &str) -> Result<(), Error> { /* ... */ }
}

3. 高変動性のコードは低結合に保つ

ビジネスロジック(Core Subdomain)は頻繁に変更される。他への影響を最小化するため、低結合に保つ必要がある。

// Strategy Patternで変動性を隔離
pub trait PricingStrategy {
    fn calculate(&self, base_price: f64) -> f64;
}

pub struct StandardPricing;
impl PricingStrategy for StandardPricing {
    fn calculate(&self, base_price: f64) -> f64 {
        base_price // ビジネスルールの変更がここに限定される
    }
}

pub struct Order {
    pricing: Box<dyn PricingStrategy>, // Dependency Injection
}

4. Connascenceを意識したリファクタリング

パターン1: Position → Name

// Before: Connascence of Position(悪い例)
fn create_user(name: String, email: String, age: u32, country: String) -> User {
    // 引数の順序に依存
}

// After: Connascence of Name(良い例)
struct UserBuilder {
    name: String,
    email: String,
    age: u32,
    country: String,
}

impl UserBuilder {
    fn name(mut self, name: String) -> Self {
        self.name = name;
        self
    }
    // 他のフィールドも同様
}

パターン2: Meaning → Name

// Before: Connascence of Meaning(悪い例)
if status == 1 { /* active */ }
else if status == 2 { /* inactive */ }

// After: Connascence of Name(良い例)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum UserStatus {
    Active,
    Inactive,
    Suspended,
}

if status == UserStatus::Active { /* ... */ }

おわりに

Vlad Khononov の「Balancing Coupling」フレームワークが教えてくれるのは、単なる「強い/弱い」という二元論を超えた結合度の見方だ。結合の強さ、距離、変動性——この3つの軸で測定することで、より細かい粒度での分析が可能になる。

この調査を通じて明らかになったのは、Rust エコシステムには部分的に役立つツールは存在するものの、Balancing Coupling の概念を完全に体現するツールはまだ存在しないということだった。cargo-modulesrust-code-analysiscargo tree といった既存ツールは、それぞれが異なる視点からコードを照らし出してくれる。しかし、Integration Strength の分類、Dynamic Connascence の検出、変動性の分析、そしてバランススコアの計算——これらを統合的に扱うには、カスタム実装が必要だ。

そこで、本記事で紹介した設計をベースに、実際に動作するツールを作成していく。 syn クレートによる AST 解析、Git 履歴からの変動性測定、そして Khononov のフレームワークに基づくバランス評価——これらを組み合わせた実用的なツールだ。コードの「ちょうどいい距離感」を数値化し、可視化することで、リファクタリングの指針となることを目指す。

ここで忘れてはいけないのは、結合をゼロにするのが目的ではないということだ。人間関係と同じように、コードにも「適切な距離感」がある。密接に関連する機能は、むしろ強く結合すべきだ。無理に引き離せば、かえって複雑になる。

定期的な測定と分析により、過度な抽象化を避けつつ、柔軟で進化可能な設計を維持していこう。コードの「ちょうどいい距離感」は、測定することで初めて見えてくる。




以上の内容はhttps://syu-m-5151.hatenablog.com/entry/2025/10/31/125256より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14