なぜ「クラス間の依存を減らす」のが大事なのか
クラス同士がベッタリ依存していると、どこか 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;
}
}
JavaBadOrderService は 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;
}
}
JavaReportService は 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");
}
}
JavaDiscountService がすべての割引ロジックを抱え込んでいるので、
割引種類が増えるたびに 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);
}
}
JavaBadPrinter は 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()); // 結果だけ聞く
}
}
JavaGoodPrinter は「Order の中身がどうなっているか」を知りません。
「合計金額を教えて」というメソッドだけに依存しています。
この「必要なことだけ聞く」スタイルは、
クラス間の依存を減らすための、とても大事な感覚です。
まとめ:依存を減らすために、いつも意識すること
クラス間の依存を減らすために、常に自分に問いかけてほしいのは、次のようなことです。
このクラス、本当に自分で new する必要があるか?
役割(インターフェース)に依存できないか?
やり方の違いを if で抱え込まず、外から差し替えられないか?
相手の「内部構造」まで知ろうとしていないか?必要なことだけ聞けないか?
一言でまとめると、
具体クラスを直接 new しない。
インターフェース(抽象)に依存する。
コンポジション(委譲)で振る舞いを外に出す。
相手の内側を覗かず、「結果だけ」教えてもらう。
このあたりを意識すれば、クラス間の依存は目に見えて減っていきます。
