Java | オブジェクト指向:クラス間の依存を減らす方法

Java Java
スポンサーリンク

なぜ「クラス間の依存を減らす」のが大事なのか

クラス同士がベッタリ依存していると、どこか 1 クラスを少し変えただけで、他のクラスが次々壊れていきます。
「このメソッド名を変えたいだけなのに、10 クラス直さないとコンパイルが通らない」みたいな状態です。

依存を減らすというのは、
「お互いに知らなくていいことは、できるだけ知らないようにする」
ということです。

その結果として、変更の影響範囲が小さくなり、テストしやすくなり、コードの自由度も上がります。
ここから、「よくある悪い依存」→「どう直すか」を具体的な Java のコードで見ていきます。


直接 new しない(コンストラクタ注入で依存を外に出す)

悪い例:クラスの中でベタ書き new

まず典型的な「依存が強すぎる」パターンです。

final class BadOrderService {

    String place(int amount) {
        PaymentGateway gateway = new PaymentGateway();  // ここでベタ new
        boolean ok = gateway.pay(amount);
        return ok ? "OK" : "NG";
    }
}

final class PaymentGateway {
    boolean pay(int amount) {
        System.out.println("PAY " + amount);
        return true;
    }
}
Java

BadOrderService は PaymentGateway の「具体クラス名」を知っていて、さらに自分の中で new しています。
この状態だと、決済方法を差し替えたいときに BadOrderService の中身を直接いじる必要があります。

テストで「成功するゲートウェイ」「失敗するゲートウェイ」を試したくても、
差し替えができません。

良い例:コンストラクタで依存を「注入」する

依存を減らす一歩目は、「自分で new しない」ことです。
必要なものはコンストラクタの引数でもらうようにします。

interface Payment {
    boolean pay(int amount);
}

final class OrderService {

    private final Payment payment;   // 具体クラスではなく「役割」への依存

    OrderService(Payment payment) {
        this.payment = payment;
    }

    String place(int amount) {
        boolean ok = payment.pay(amount);
        return ok ? "OK" : "NG";
    }
}
Java

こうすると、OrderService は「Payment という役割」にだけ依存します。
「どの決済方法を使うか」は、外側で決めて渡すことができます。

実際に使うときのコード例はこんな感じです。

final class StripePayment implements Payment {
    @Override
    public boolean pay(int amount) {
        System.out.println("Stripe " + amount);
        return true;
    }
}

OrderService service = new OrderService(new StripePayment());
Java

テスト時には、偽物の Payment を渡せます。

final class FakePayment implements Payment {
    @Override
    public boolean pay(int amount) {
        return true;   // 何でも成功するテスト用
    }
}

OrderService service = new OrderService(new FakePayment());
Java

依存を減らす、というのは
「クラス自身が“どの具体クラスを使うか”まで抱え込まないようにする」
ということでもあります。


インターフェース(抽象)に依存する(重要)

悪い例:具体クラスにべったり

次は「具体クラスに直接べったり依存している」例です。

final class ReportService {

    String build(String raw) {
        CsvFormatter formatter = new CsvFormatter();  // 具体クラスを直に new
        return formatter.format(raw);
    }
}

final class CsvFormatter {
    String format(String raw) {
        return raw == null ? "" : "CSV:" + raw;
    }
}
Java

ReportService は CsvFormatter にべったり依存しているので、
「CSV 以外の形式にしたい」たびに、ReportService を修正しないといけません。

良い例:役割(インターフェース)だけを見る

インターフェースで「フォーマットするという役割」だけを定義し、
ReportService はそこにだけ依存します。

interface Formatter {
    String format(String raw);
}

final class ReportService {

    private final Formatter formatter;

    ReportService(Formatter formatter) {
        this.formatter = formatter;
    }

    String build(String raw) {
        return formatter.format(raw);
    }
}
Java

具体的なフォーマッタは別クラスにします。

final class CsvFormatter implements Formatter {
    @Override
    public String format(String raw) {
        return raw == null ? "" : "CSV:" + raw;
    }
}

final class JsonFormatter implements Formatter {
    @Override
    public String format(String raw) {
        return raw == null ? "" : "{\"value\":\"" + raw + "\"}";
    }
}
Java

この形だと、

本番では new ReportService(new CsvFormatter())
別の用途では new ReportService(new JsonFormatter())

のように、外側で自由に差し替えられます。
ReportService は「Formatter という抽象」にしか依存していないので、
具体クラスが何個増えても、ReportService 自体は変えずに済みます。

依存を減らすうえで、
「具体クラスではなくインターフェース(抽象)を見てコードを書く」
というのは非常に強力なテクニックです。


コンポジション(委譲)で「やり方の違い」を外に出す

悪い例:巨大な if/else で具体型に依存する

よくあるパターンとして、「1 クラスの中で、いくつものパターンを if で分岐」しているケースがあります。

final class DiscountService {

    int apply(int price, String type) {
        if ("none".equals(type)) {
            return price;
        } else if ("rate".equals(type)) {
            return price - price * 10 / 100;
        } else if ("fixed".equals(type)) {
            return price - 100;
        }
        throw new IllegalArgumentException("unknown type");
    }
}
Java

DiscountService がすべての割引ロジックを抱え込んでいるので、
割引種類が増えるたびに apply が太っていきます。
このメソッドにどんどん依存が集まり、変更が怖くなります。

良い例:戦略オブジェクトに委譲する

「割引のやり方」をインターフェースにして、
DiscountService は「どの戦略を使うか」を外から受け取り、ただ委譲します。

interface Discount {
    int apply(int price);
}

final class NoDiscount implements Discount {
    @Override
    public int apply(int price) {
        return price;
    }
}

final class RateDiscount implements Discount {
    private final int percent;
    RateDiscount(int percent) { this.percent = percent; }

    @Override
    public int apply(int price) {
        return price - price * percent / 100;
    }
}

final class FixedDiscount implements Discount {
    private final int amount;
    FixedDiscount(int amount) { this.amount = amount; }

    @Override
    public int apply(int price) {
        return price - amount;
    }
}

final class DiscountService {

    private final Discount discount;

    DiscountService(Discount discount) {
        this.discount = discount;
    }

    int apply(int price) {
        return discount.apply(price);  // 委譲
    }
}
Java

こうすると、DiscountService 自体は「割引戦略を使うだけ」の薄いクラスになります。
新しい割引ロジックを追加しても、DiscountService はいじらなくて済みます。

これは「継承」ではなく「コンポジション(委譲)」で振る舞いを差し替えるパターンで、
クラス間の依存を弱く保つために非常に有効です。


「知りすぎない」ようにする(法則を意識する)

依存を増やす「知りすぎクラス」

クラスが他のクラスの内部構造まで知ろうとし始めると、依存が一気に増えます。

final class BadPrinter {

    void printOrder(Order order) {
        // 本来 Order に任せるべき内部の構造を知りすぎている例
        int total = 0;
        for (OrderLine line : order.getLines()) {
            total += line.getUnitPrice() * line.getQuantity();
        }
        System.out.println("total=" + total);
    }
}
Java

BadPrinter は Order の構造(明細リストの存在、金額の計算方法)にベタベタに依存しています。
Order の内部が少し変わると(例えば送料無料条件を加えた、税計算を加えたなど)、
BadPrinter も巻き込まれて変更が必要になります。

良い例:必要なことだけを「聞く」

内部の構造ではなく、「欲しい結果だけを教えてもらう」ようにします。

final class Order {

    private final java.util.List<OrderLine> lines = new java.util.ArrayList<>();

    void addLine(String name, int unitPrice, int quantity) {
        lines.add(new OrderLine(name, unitPrice, quantity));
    }

    int totalPrice() {
        return lines.stream().mapToInt(OrderLine::linePrice).sum();
    }
}

final class OrderLine {

    private final String name;
    private final int unitPrice;
    private final int quantity;

    OrderLine(String name, int unitPrice, int quantity) {
        this.name = name;
        this.unitPrice = unitPrice;
        this.quantity = quantity;
    }

    int linePrice() {
        return unitPrice * quantity;
    }
}

final class GoodPrinter {

    void printOrder(Order order) {
        System.out.println("total=" + order.totalPrice());  // 結果だけ聞く
    }
}
Java

GoodPrinter は「Order の中身がどうなっているか」を知りません。
「合計金額を教えて」というメソッドだけに依存しています。

この「必要なことだけ聞く」スタイルは、
クラス間の依存を減らすための、とても大事な感覚です。


まとめ:依存を減らすために、いつも意識すること

クラス間の依存を減らすために、常に自分に問いかけてほしいのは、次のようなことです。

このクラス、本当に自分で new する必要があるか?
役割(インターフェース)に依存できないか?
やり方の違いを if で抱え込まず、外から差し替えられないか?
相手の「内部構造」まで知ろうとしていないか?必要なことだけ聞けないか?

一言でまとめると、

具体クラスを直接 new しない。
インターフェース(抽象)に依存する。
コンポジション(委譲)で振る舞いを外に出す。
相手の内側を覗かず、「結果だけ」教えてもらう。

このあたりを意識すれば、クラス間の依存は目に見えて減っていきます。

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