依存関係と依存の方向¶
一言で言うと¶
依存(dependency)とは「A のコードが B を知っている(B が変わると A が壊れうる)」関係。依存の方向を制御することが設計の中心技術です。原則は「変わりやすいものに依存しない。変わりにくいものに依存させる」。
解決したい問題¶
依存は避けられません(何にも依存しないコードは何もできない)。問題は、
- 依存が多すぎる: 1つの変更が大量のクラスに波及する
- 依存の方向が悪い: 安定していてほしい中核ロジックが、変わりやすい末端(UI、保存形式)に依存している
- 依存が循環している: A→B→A。どちらも単独で変更・テスト・再利用できない
「依存している」とは具体的に何か¶
C++では以下があれば依存です。
#include "Enemy.h" // includeは依存(Enemy.hの変更で再コンパイル)
class Radar {
Enemy* target_; // 型を知っている(生ポインタ: 所有せず参照するだけ)
void Scan(Enemy& e); // 引数の型
Enemy Make(); // 戻り値の型
};
C#でも同じで、using して型名を書けば依存です。「B が変わったとき A を再確認する必要があるか?」が判定基準です。
問題のある実装(方向が悪い例)¶
戦闘の中核ロジックが、変わりやすいUIに依存しています。
// C++20(問題例)
#include "HpBarWidget.h" // 中核ロジックがUI実装をinclude
class BattleLogic {
public:
void ApplyDamage(int amount) {
hp_ -= amount;
hpBar_->SetFillRate(static_cast<float>(hp_) / maxHp_); // UIを直接更新
}
private:
int hp_ = 100, maxHp_ = 100;
HpBarWidget* hpBar_; // 生ポインタ: UIの寿命はUIシステムが管理しているため所有しない
};
何が問題になるか¶
- HPバーのデザイン変更(Widget差し替え)のたびに BattleLogic を触る。安定していてほしい側が、変わりやすい側の都合で変更される。
- UIなしで戦闘ロジックをテストできない(ヘッドレスのバランス調整シミュレーションが書けない)。
- 依存の連鎖: BattleLogic → HpBarWidget → 描画エンジン。戦闘のテストに描画エンジンが必要になる。
改善した実装(依存の向きを逆転する)¶
「通知を受けたい側(UI)」が「中核(戦闘)」に依存するように向きを変えます。
// C++20(骨格)
#include <functional>
#include <vector>
class BattleLogic {
public:
// UIのことは知らない。「HPが変わった」と通知するだけ
std::function<void(int hp, int maxHp)> onHpChanged; // 単純化のため1購読者。複数ならvector
void ApplyDamage(int amount) {
hp_ -= amount;
if (onHpChanged) onHpChanged(hp_, maxHp_);
}
private:
int hp_ = 100, maxHp_ = 100;
};
// UI側(変わりやすい側)が、中核(変わりにくい側)に依存して購読する
void SetupUi(BattleLogic& battle, HpBarWidget& bar) {
battle.onHpChanged = [&bar](int hp, int maxHp) {
bar.SetFillRate(static_cast<float>(hp) / maxHp);
};
}
依存の向き: 変更前 BattleLogic → HpBarWidget、変更後 UI設定コード → BattleLogic。中核は誰にも依存しなくなりました。この「抽象やコールバックを挟んで依存の向きを逆にする」技術を原則として定式化したものが DIP(依存性逆転の原則) です。
何が改善されたか¶
- UIをいくら作り替えても BattleLogic は無変更・再コンパイル不要。
- BattleLogic 単体でテスト可能(コールバックにテスト用ラムダを渡すだけ)。
代わりに何を失ったか¶
- 「ダメージを与えたらUIがどう動くか」を追うのに、購読設定のコードを探す必要がある。
- コールバックの寿命管理という新しい注意点が生まれた(barが先に破棄されたら? → dangling pointer)。
循環依存¶
Player → Inventory → Item → Player(装備者を知りたい)のような循環は、C++ではコンパイルすら通らないことがあり(includeの循環)、通っても全クラスが一蓮托生になります。対処:
- 片方をイベント通知に変える(上の手法)
- 共通部分を第三のクラスに抽出して両方がそれに依存する
- C++なら前方宣言でinclude循環だけは切れる(設計上の循環は残る)
図解¶
悪い: [BattleLogic] ──依存──> [HpBarWidget] ──> [描画エンジン]
(安定してほしい) (よく変わる)
良い: [UI設定コード] ──依存──> [BattleLogic]
[HpBarWidget] <──呼ばれるだけ(コールバック)
よく変わる側が、安定した側に依存する
Unity/C# との対応¶
- ScriptableObject や event を使って「ロジックがUIを知らない」構造にするのが定石。
- Assembly Definition で依存の方向を物理的に強制できます(参照を張らなければコンパイルエラー)。C++のライブラリ分割に相当(→ ヘッダー分割とAssembly Definition)。
Unreal Engine との対応¶
- Module 分割(Build.cs の依存リスト)が依存方向の強制に対応します(→ UBTとModule)。
- Actor間の直接参照より Delegate / Interface 経由が推奨されるのも同じ理屈です。
よくある誤解¶
- 「依存は悪」ではありません。悪いのは方向と量です。具象クラスへの素直な依存は、変更されにくい相手なら全く問題ありません。
- 「とにかくインターフェースを挟めば良い」でもありません。変わらない相手に抽象を挟むのはコストだけ払う行為です(→ 判断ガイド)。
使う場面 / 使わない場面¶
- 向きを制御すべき場面: 中核ロジックと、UI・保存・通信・エンジンAPIのような「変わりやすい末端」との境界。テストしたい境界。
- 放置してよい場面: 同じ理由で一緒に変わる仲間内(EnemyがEnemyBulletに依存する等)。標準ライブラリやエンジンの安定APIへの依存。
関連項目¶
理解度チェック¶
- 「AがBに依存する」の実務的な判定基準は何ですか。
- 「依存の方向が悪い」とはどういう状態ですか。ゲームの例で言えますか。
- 循環依存を切る方法を2つ挙げてください。
演習¶
Quest → Player → Inventory → Quest(クエストアイテム所持チェックのため)という循環があるとします。どこをどう切るか、2案作って利点欠点を比較してください(類題の解答: 第1部演習)。
前: 凝集度と結合度 | カテゴリ目次 | 次: 抽象化・カプセル化・情報隠蔽