依存の方向とは
「依存の方向」は、あるコードがどちら側(上位/下位、抽象/具体)へ向かって頼っているかを示す考え方です。呼び出し側が別のクラスやライブラリに依存するとき、その“向き”が正しいかどうかで保守性と拡張性が決まります。筋の良い設計では、高水準(ビジネスロジック)が低水準(具体実装)に直接依存せず、抽象(インターフェース)へ依存します。下位の詳細は、その抽象を満たす側に回ります。
正しい向きの原則(重要)
高水準のポリシーは「抽象」に依存し、低水準の詳細は「抽象を実装」する——これが依存の基本原則です。こうして“上→下”の直接依存を避けると、差し替えやテストが容易になり、変更が上位へ波及しなくなります。設計の合言葉は「ポリシーは契約へ、詳細は契約を満たす」です。
// 抽象(契約)
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 PurchaseService {
private final PaymentGateway gateway; // 依存の向き:高水準 -> 抽象
PurchaseService(PaymentGateway gateway) { this.gateway = gateway; } // 外部から注入
boolean buy(int amount) { return amount > 0 && gateway.charge(amount); }
}
Javaこの向きを守ると、Gateway の実装を差し替えても PurchaseService は不変で、テストではフェイク実装を渡すだけで検証できます。
悪い向きとその痛み(深掘り)
高水準が低水準に“直接”依存すると、変更が上位まで伝播します。テストは外部接続に引きずられ、ユースケースの検証が難しくなります。循環依存(互いに依存し合う)を作ると、変更不能・ビルド困難の温床になります。
// 悪い例:高水準が具体を new(強い結合、向きが逆)
final class BadPurchaseService {
boolean buy(int amount) {
var gw = new StripeGateway(); // 具体に固定
return amount > 0 && gw.charge(amount);
}
}
Javaこの形では他の決済手段への切り替えやテスト用の差し替えが困難です。抽象+注入へ改めるのが筋です。
依存の方向を揃える具体テクニック(重要)
インターフェースで境界を作る
境界(ポート)をインターフェースとして定義し、上位はその契約に依存します。下位(アダプタ)は契約を implements して外部世界へ接続します。
// ドメイン側(ポート=抽象)
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コンストラクタ注入で“向き”を固定する
具体の new を高水準側で行わず、外から渡してもらう(DI)。依存が外部化され、差し替えとテストが容易になります。
単一責務のインターフェースへ分割する
巨大な契約は依存を重くします。必要な役割を小さく切り、上位は必要最小限へ依存します。向きが揃い、絡みが減ります。
層構造と矢印の向き
UI→アプリケーション→ドメイン→インフラのように層を分け、矢印は「上位→下位の抽象」に向けます。下位の具体(インフラ)は上位に依存せず、抽象(ポート)を実装します。循環依存は作らないのが鉄則です。
// 層のイメージ(コードで表現)
interface Storage { void put(String key, String value); } // ドメイン/アプリの抽象
final class FileStorage implements Storage { // インフラの具体
@Override public void put(String key, String value) { System.out.println("write " + key + "=" + value); }
}
final class SaveUseCase { // アプリケーション
private final Storage st;
SaveUseCase(Storage st) { this.st = st; } // 抽象へ依存(矢印の向き)
void saveAll() { st.put("a","1"); st.put("b","2"); }
}
Java例題で体感する“向き”の効き目
差し替え自由なフォーマッタ
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依存の向きが正しいため、実装追加・切替・テストが一切上位へ波及しません。
外部資源をポートで隔離
interface Clock { java.time.Instant now(); } // 抽象(ポート)
final class SystemClock implements Clock {
@Override public java.time.Instant now(){ return java.time.Instant.now(); }
}
final class ReportService {
private final Clock clock; // 抽象へ依存
ReportService(Clock clock){ this.clock = clock; }
String stamp(){ return clock.now().toString(); } // テストでフェイクを注入可能
}
Java外部資源をポートに追い出すと、上位は純粋に保たれ、テスト容易性が向上します。
落とし穴と守るべき型(重要部分のまとめ)
依存の方向が逆(高水準→具体)だと変更が波及し、テストが難しく、循環依存の危険も高まります。正しい向きは「高水準→抽象」「低水準→抽象を実装」。具体の new は外へ追い出し、コンストラクタ注入で固定する。インターフェースは単一責務に分け、外部資源はポート/アダプタで境界化。層間の矢印は上位から下位の抽象へ、循環は作らない——この型を守れば、差し替え自由で壊れにくい設計になります。
