Java | オブジェクト指向:コンストラクタインジェクション

Java Java
スポンサーリンク

コンストラクタインジェクションとは

コンストラクタインジェクションは
「そのクラスが必要とするオブジェクト(依存)を、コンストラクタの引数で受け取る」
という設計のやり方です。

もっと噛み砕くと、

・自分の中で new して相棒を決めない
・「誰と組むか」は外から渡してもらう
・コンストラクタの時点で「必要な相棒は全部そろっている」状態にする

という考え方です。

これにより、クラス同士の結びつきが弱くなり、差し替えやテストが圧倒的にやりやすくなります。


まずは「よくない書き方」から見る

自分の中で new してしまう例

次のコードを見てください。

final class EmailSender {

    void send(String to, String body) {
        System.out.println("SEND MAIL to=" + to + " body=" + body);
    }
}

final class UserService {

    String register(String email) {
        EmailSender sender = new EmailSender();  // ここで new
        sender.send(email, "Welcome!");

        return "OK";
    }
}
Java

一見シンプルですが、UserServiceEmailSender にベッタリ依存しています。

問題点は、
「メール送信の方法を変えたい」「テストではメール送信をフェイクにしたい」
と思ったときに、UserService の中身を書き換える必要があることです。

さらにテストを書くとき、本当にメールが送られてしまうかもしれません。
「呼び出しただけ確認したい」「送信はしたくない」がやりにくい状態です。


コンストラクタインジェクションの基本形(重要)

役割をインターフェースにし、コンストラクタで受け取る

上の例を、コンストラクタインジェクションを使って書き直してみます。

まず、「メールを送る」という役割をインターフェースに切り出します。

interface Mailer {
    void send(String to, String body);
}
Java

そして、UserService はこの Mailer にだけ依存します。

final class UserService {

    private final Mailer mailer;    // ここでは役割の型だけを持つ

    UserService(Mailer mailer) {    // コンストラクタで注入
        this.mailer = mailer;
    }

    String register(String email) {
        mailer.send(email, "Welcome!");
        return "OK";
    }
}
Java

ここがコンストラクタインジェクションのポイントです。

UserService は「Mailer が必要です」と宣言するだけで、
「どの実装クラスか」は一切知りません。
誰と組むかは、インスタンスを作る側が決めます。

final class ConsoleMailer implements Mailer {
    @Override
    public void send(String to, String body) {
        System.out.println("SEND MAIL to=" + to + " body=" + body);
    }
}
Java

実際に使うときは、こうなります。

Mailer mailer = new ConsoleMailer();
UserService service = new UserService(mailer);  // コンストラクタで注入
service.register("test@example.com");
Java

UserService の中では、一度も new ConsoleMailer() を書いていません。
「誰と組むか」は外で決めて、コンストラクタの引数で注入しています。


なぜコンストラクタインジェクションが強いのか(深掘り)

必須の依存を「取り忘れ不可能」にできる

コンストラクタインジェクションでは、必要な相手をコンストラクタの引数にします。

final class OrderService {

    private final Payment payment;

    OrderService(Payment payment) {   // ここで必須依存を強制
        this.payment = payment;
    }
}
Java

この設計だと、Payment を渡さずに OrderService を作ることはできません。
コンパイル時点で「依存の注入忘れ」を防げます。

一方、セッターで注入するスタイルだと、

final class OrderService {

    private Payment payment;

    void setPayment(Payment payment) {
        this.payment = payment;
    }
}
Java

「setPayment を呼び忘れる」というバグが起き得ます。
コンストラクタインジェクションは、「必ず必要な依存」を保証できるのが大きなメリットです。

フィールドを final にできる

コンストラクタでしか設定しないので、フィールドを final にできます。

final class OrderService {

    private final Payment payment;

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

一度セットされたら、途中で差し替えられません。
「いつの間にか別のオブジェクトにすり替わっている」といった怖さがなくなります。
この「不変性」は、バグを減らすうえで非常に強い味方です。

「何が必要なクラスか」が一目で分かる

コンストラクタの引数を見れば、そのクラスが何に依存しているかが一目で分かります。

final class PlaceOrderUseCase {

    private final OrderRepository repo;
    private final Payment payment;

    PlaceOrderUseCase(OrderRepository repo, Payment payment) {
        this.repo = repo;
        this.payment = payment;
    }
}
Java

このクラスは「注文を保存するもの」と「支払いをするもの」が必要なんだな、ということが
コンストラクタを見るだけで分かります。

これは「設計書」よりも確かな情報です。
コンストラクタは、クラスの「依存関係の顔」になってくれます。


テストとの相性:偽物を注入するだけでテストできる

本物を使いたくないときにどうするか

先ほどの UserService を思い出してください。

final class UserService {

    private final Mailer mailer;

    UserService(Mailer mailer) {
        this.mailer = mailer;
    }

    String register(String email) {
        mailer.send(email, "Welcome!");
        return "OK";
    }
}
Java

これをテストしたいとき、実際のメール送信をしたくない場合がほとんどです。
そこで「テスト用の Mailer」を作ります。

final class FakeMailer implements Mailer {

    String lastTo;
    String lastBody;

    @Override
    public void send(String to, String body) {
        this.lastTo = to;
        this.lastBody = body;
    }
}
Java

テストコードのイメージはこうなります。

FakeMailer fake = new FakeMailer();
UserService service = new UserService(fake);  // コンストラクタで偽物注入

service.register("test@example.com");

// FakeMailer に記録された内容を検証
assert "test@example.com".equals(fake.lastTo);
assert "Welcome!".equals(fake.lastBody);
Java

本物のメール送信は一切行われず、
「UserService が Mailer の send を正しい引数で呼んだか」だけを検証できます。

コンストラクタインジェクションをしていないと、この差し替えが困難です。
中で new していたら、どう頑張っても偽物にできません。


よくある「やってしまいがち」と改善例

シングルトンで直接参照してしまう

ありがちなパターンとして、次のようなコードがあります。

final class Logger {
    private static final Logger INSTANCE = new Logger();
    public static Logger getInstance() { return INSTANCE; }

    void log(String message) {
        System.out.println(message);
    }
}

final class Service {

    void doSomething() {
        Logger.getInstance().log("start");  // どこからでも直接参照
    }
}
Java

これは依存を「隠れた形」で持ってしまっています。
Service は Logger に依存しているのに、コンストラクタには何も出てきません。

改善するなら、Logger も依存として注入します。

interface Log {
    void log(String message);
}

final class ConsoleLog implements Log {
    @Override
    public void log(String message) {
        System.out.println(message);
    }
}

final class Service {

    private final Log log;

    Service(Log log) {
        this.log = log;
    }

    void doSomething() {
        log.log("start");
    }
}
Java

こうしておくと、テスト時には「記録用のフェイク Log」を渡せますし、
本番ではファイル出力ログに切り替える、といった差し替えも簡単です。

「引数が増えるのがイヤ」でコンストラクタインジェクションを避ける

「コンストラクタの引数が多くなりそうでイヤだ」という理由で、
コンストラクタインジェクションを避ける人もいます。

しかし、引数が多いのは「コンストラクタインジェクションのせい」ではなく
「そのクラスが多くの依存を持ちすぎている」サインです。

コンストラクタインジェクションを入れてみると、
「このクラス、こんなにいろいろに依存してたのか」と気付けるのがメリットでもあります。

その場合は、

責務を分けてクラスを小さくする
中間層(ファサード)を作って依存をまとめる

といったリファクタリングを検討するきっかけになります。


DI コンテナとコンストラクタインジェクション

Spring などの DI コンテナを使うとき、多くのフレームワークは
「コンストラクタインジェクション」を標準的なやり方としてサポートしています。

アノテーションで書くと、だいたい次のような形です(ざっくりイメージ)。

@Component
class UserService {

    private final Mailer mailer;

    @Autowired
    UserService(Mailer mailer) {   // フレームワークがここに注入してくれる
        this.mailer = mailer;
    }
}
Java

フレームワークがやっているのは、

どのクラス(Mailer の実装)を new するか決める
UserService を new するとき、そのインスタンスをコンストラクタに渡す

という、手動でやったことを自動化しているだけです。

だからこそ、先に「生の Java」でコンストラクタインジェクションの感覚を身につけると、
フレームワークの動きも自然に理解できるようになります。


まとめ:コンストラクタインジェクションを設計の標準にする

コンストラクタインジェクションは、依存を見える化し、
テストしやすく、変更にも強いクラスを作るための“標準フォーム”です。

自分のクラスで、

中で new してしまっている依存はないか
シングルトンを直接呼びに行ってないか
本当はインターフェース(役割)に依存できないか
コンストラクタで全部渡してもらう形にできないか

を一つずつ見直してみてください。

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