依存関係とは
依存関係は「あるコードが、他のコードの存在や振る舞いを前提にして成り立っている状態」です。クラスが別のクラスを new したり、そのメソッドを呼んだり、型として参照したりすると、そこで依存が発生します。依存は悪ではありませんが、強すぎると変更の波及が大きくなり、保守やテストが難しくなります。オブジェクト指向では「依存の向き」と「依存の種類」を意識して、柔らかい(疎結合な)設計に整えるのが鍵です。
依存の種類と“強さ”
コンパイル時依存と実行時依存
コンパイル時依存は、型参照(フィールド・引数・戻り値)やメソッド呼び出しによって「その型定義が必要」になる関係です。実行時依存は、実際に new して使う具体クラスや外部リソース(ファイル、DB、ネットワーク)への依存です。型を抽象(インターフェース)に寄せると、コンパイル時依存の重さが下がり、実行時の差し替えが容易になります。
強い依存と弱い依存
具象クラスを直接 new し、内部詳細に踏み込むほど強い依存です。インターフェース経由で必要最小限の契約に絞ると弱い依存になります。弱い依存は変更に強く、テストや差し替えが簡単になります。
依存の“向き”が設計を決める(重要)
高水準は抽象に依存し、詳細は抽象を満たす
上位のポリシー(ビジネスルール)を持つコードは、具体実装ではなく「抽象(インターフェース)」に依存させます。具体はその抽象を implements して従う側に回ります。こうすると上位は下位の変更に引きずられず、差し替えが自由になります。
interface PaymentGateway { boolean charge(int amount); } // 抽象(契約)
final class StripeGateway implements PaymentGateway {
@Override public boolean charge(int amount) { System.out.println("stripe " + amount); return true; }
}
final class Service {
private final PaymentGateway gw; // 抽象に依存
Service(PaymentGateway gw) { this.gw = gw; } // 依存は“注入”
boolean buy(int amount) { return amount > 0 && gw.charge(amount); }
}
Java上位(Service)は抽象にだけ依存し、下位(StripeGateway)は抽象を満たす。向きが正しく、差し替えが容易です。
依存を弱くする具体テクニック(重要な深掘り)
コンストラクタ注入(DI)
必要な依存を“外から渡す”ことで、クラス自身が具体を new する強い結合を避けます。テストではフェイク実装を渡せるため、外部接続なしで検証できます。
final class App {
private final Formatter formatter; // 抽象
App(Formatter formatter) { this.formatter = formatter; }
String run(String s) { return formatter.format(s); }
}
Javaインターフェース分割(単一責務)
「巨大な1インターフェース」に依存するより、必要な役割だけを小さく切って依存します。こうすると差し替えやテストが簡単になり、実装の自由度も上がります。
委譲と合成(継承よりも)
継承は強い依存(親の内部契約に縛られる)になりがちです。差し替えたい振る舞いは“持つ(委譲)”に変えると、依存の粒度を下げて柔らかくできます。
よくある悪い依存とリファクタ例
悪い例:具体の new をクラス内部で行う
final class BadService {
String buy(int amount) {
var gw = new StripeGateway(); // 具体に固定(強い依存)
return gw.charge(amount) ? "OK" : "NG";
}
}
Javaテストや差し替えが困難です。抽象+注入に改めます。
interface PaymentGateway { boolean charge(int amount); }
final class GoodService {
private final PaymentGateway gw; // 抽象+注入
GoodService(PaymentGateway gw) { this.gw = gw; }
String buy(int amount) { return gw.charge(amount) ? "OK" : "NG"; }
}
Java悪い例:内部詳細に触る“友達”クラス
protected フィールドや内部コレクションを直接触ると、変更が波及します。アクセサや小さな操作メソッドを通して契約化し、内部構造を隠蔽します。
層と循環依存の回避
層を分け、循環を作らない
「UI → アプリケーション → ドメイン → インフラ」のように層を定義し、上位は下位に依存しても、下位が上位へ依存する循環を作らないこと。循環は変更不能・テスト困難の温床です。必要ならポート/アダプタ(抽象)を間に置き、向きを揃えます。
// ドメイン側(ポート=抽象)
interface MailPort { void send(String to, String body); }
// インフラ側(アダプタ=具体)
final class SmtpMailAdapter implements MailPort {
@Override public void send(String to, String body) { System.out.println("SMTP " + to); }
}
// アプリケーション(抽象に依存)
final class NotifyUseCase {
private final MailPort mail;
NotifyUseCase(MailPort mail) { this.mail = mail; }
void notify(String to) { mail.send(to, "hello"); }
}
Java外部依存(ファイル・DB・ネット)との付き合い方
外部資源は変更・障害の影響が大きく、テストも難しい“重い依存”です。ポート(インターフェース)に切り出し、実体は境界外に置いて注入します。ユースケースやドメインは外部詳細に触れず、契約越しにやり取りします。これでオフラインテストやフェイルセーフ設計が容易になります。
例題で体感する依存の整理
例 1: フォーマッタの戦略注入で分岐を消す
interface Formatter { String format(String raw); }
final class UpperFormatter implements Formatter {
@Override public String format(String raw){ return raw == null ? "" : raw.toUpperCase(); }
}
final class SnakeFormatter implements Formatter {
@Override public String format(String raw){ return raw == null ? "" : raw.trim().replaceAll("\\s+", "_"); }
}
final class NameService {
private final Formatter formatter; // 抽象に依存
NameService(Formatter f){ this.formatter = f; }
String render(String s){ return formatter.format(s); } // 差し替え容易
}
Java例 2: ストレージの依存を抽象化
interface Storage { void put(String key, String value); }
final class MemoryStorage implements Storage {
private final java.util.Map<String,String> map = new java.util.HashMap<>();
@Override public void put(String key, String value){ map.put(key, value); }
}
final class FileStorage implements Storage {
@Override public void put(String key, String value){ System.out.println("write " + key + "=" + value); }
}
void saveAll(Storage st) { st.put("a","1"); st.put("b","2"); } // 呼び出し側は抽象だけ
Java落とし穴と守るべき型(重要部分のまとめ)
強い依存(具体 new、内部詳細露出、循環)は変更の波及を招きます。依存は“抽象へ向け”、注入で外側から与える。インターフェースは小さく分け、継承より委譲を優先し、内部構造は隠蔽する。外部資源はポート/アダプタで境界に追い出し、ユースケースは契約だけに依存する。この型を守ると、テスト容易性が上がり、拡張が安全になります。
