Java | オブジェクト指向:依存関係とは

Java Java
スポンサーリンク

依存関係とは

依存関係は「あるコードが、他のコードの存在や振る舞いを前提にして成り立っている状態」です。クラスが別のクラスを 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、内部詳細露出、循環)は変更の波及を招きます。依存は“抽象へ向け”、注入で外側から与える。インターフェースは小さく分け、継承より委譲を優先し、内部構造は隠蔽する。外部資源はポート/アダプタで境界に追い出し、ユースケースは契約だけに依存する。この型を守ると、テスト容易性が上がり、拡張が安全になります。

タイトルとURLをコピーしました