はじめに
人間関係が数値化できればなぁって思ったこともありますか?僕はあります。「この人とは、ちょうどいい距離感だな」とか、「もうちょっと親しくなりたいけど、近づきすぎると息苦しいかもしれない」とか。そういう、言葉にしづらい感覚を、もし数字で表せたら——なんて。
コードを書いているとき、似たようなことを考える。
「このモジュールとあのモジュール、近すぎるな」と思う瞬間がある。あるいは逆に、「これ、もっと近くにあった方がいいんじゃないか」と。モジュール同士の距離感。誰もが一度は悩んだことがあるはずだ。近すぎても、遠すぎてもいけない。でも、「ちょうどいい」って、どういうことだろう。
バグを修正したときのことだ。直接的には関係ないはずの、別の場所が壊れる。そんな経験、ないだろうか。それは、見えない糸が張り巡らされている証拠だ。データの持ち方への暗黙の依存。前提条件。実行順序。そういう、目に見えにくい結合が、コードのあちこちに潜んでいる。
大切なのは、結合をゼロにすることじゃない。そんなことは、できない。むしろ、適切にバランスさせることだ。人間関係と同じように。
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(変動性)
コンポーネントの変更頻度を示す。
- Core Subdomain(コアサブドメイン)- 高頻度で変更
- Supporting Subdomain(サポートサブドメイン)- 中程度の変更
- Generic Subdomain(汎用サブドメイン)- 低頻度の変更
バランスの公式
これらを組み合わせた「バランスの方程式」は、概念的には次のように表現できる。
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 Value(CoV)- 値の同期的変更への依存
- 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
特徴:
2. rust-code-analysis
Mozilla が開発した、多言語対応のコードメトリクス計測ツール。
インストール:
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 組み込みの依存関係ツリー表示コマンド。
使い方:
# 依存関係ツリーの表示 cargo tree # 深さを制限 cargo tree --depth 1 # 特定のパッケージを除外 cargo tree --prune serde # 重複する依存関係を表示 cargo tree --duplicates # リバース依存関係(何がこのクレートに依存しているか) cargo tree --invert <package-name>
4. その他の有用なツール
- cargo-deps - GraphViz DOT ファイルを生成
- cargo-depgraph - 視覚的な依存関係グラフ
- tokei - コード統計(行数、言語別集計)
- cargo-deny - 依存関係のポリシー検証
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-modules、rust-code-analysis、cargo tree といった既存ツールは、それぞれが異なる視点からコードを照らし出してくれる。しかし、Integration Strength の分類、Dynamic Connascence の検出、変動性の分析、そしてバランススコアの計算——これらを統合的に扱うには、カスタム実装が必要だ。
そこで、本記事で紹介した設計をベースに、実際に動作するツールを作成していく。 syn クレートによる AST 解析、Git 履歴からの変動性測定、そして Khononov のフレームワークに基づくバランス評価——これらを組み合わせた実用的なツールだ。コードの「ちょうどいい距離感」を数値化し、可視化することで、リファクタリングの指針となることを目指す。
ここで忘れてはいけないのは、結合をゼロにするのが目的ではないということだ。人間関係と同じように、コードにも「適切な距離感」がある。密接に関連する機能は、むしろ強く結合すべきだ。無理に引き離せば、かえって複雑になる。
定期的な測定と分析により、過度な抽象化を避けつつ、柔軟で進化可能な設計を維持していこう。コードの「ちょうどいい距離感」は、測定することで初めて見えてくる。