Java | オブジェクト指向:セッターインジェクション

Java Java
スポンサーリンク

セッターインジェクションとは何か

セッターインジェクションは
「必要な相手(依存オブジェクト)を、コンストラクタではなく setter メソッドで後から差し込む」
という DI(依存性注入)のやり方です。

コンストラクタインジェクションが
「作る瞬間に、必要なものを全部渡してもらう」
なのに対して、セッターインジェクションは
「まずは空っぽで作っておいて、そのあとで必要なものをセットする」
というイメージです。

使う場面さえ間違えなければ便利ですが、「必須の依存」に使うと危険にもなります。
ここをきっちり分けて理解することが大事です。


まずは「直接 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 を自分の中で new しています。
この状態の問題は、次のような点です。

メールの送り方を変えたくなったら UserService を修正しないといけないこと。
テストで「メールを実際には送らずに、呼ばれたかだけ確認したい」のに、差し替えようがないこと。

依存性注入は、こういう「中で勝手に相棒を決めてしまっている状態」をやめて、
外から渡してもらえるようにする考え方です。


セッターインジェクションの基本形

コンストラクタではなく setter で依存を差し込む

まず「メールを送る」という役割をインターフェースに分けます。

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

次に、UserService にセッターインジェクションを導入します。

final class UserService {

    private Mailer mailer;   // ここではまだ null かもしれない

    void setMailer(Mailer mailer) {   // セッターで注入
        this.mailer = mailer;
    }

    String register(String email) {
        mailer.send(email, "Welcome!");   // 注入されたものを使う
        return "OK";
    }
}
Java

使うときは、次のようになります。

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

UserService service = new UserService();                // まずは空で new
service.setMailer(new ConsoleMailer());                 // あとから差し込む
service.register("test@example.com");
Java

ポイントは、「誰と組むか(どの Mailer を使うか)」を UserService 自身は知らないことです。
どの実装を使うかは、外側で決めて setter で注入しています。
これがセッターインジェクションの基本イメージです。


コンストラクタインジェクションとの違い(重要)

必須か任意か、という線引き

コンストラクタインジェクションとの一番大きな違いは、「必須かどうか」です。

コンストラクタインジェクションは、
「このクラスは、これがないと成立しない」という必須依存に向いています。

final class UserService {

    private final Mailer mailer;

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

一方セッターインジェクションは、
「あとから設定する」「場合によっては設定しないこともある」
といった“任意の依存”に向いています。

final class UserService {

    private Mailer mailer;         // null かもしれない

    void setMailer(Mailer mailer) {
        this.mailer = mailer;
    }
}
Java

セッターインジェクションを「必須依存」に使うと、

setMailer を呼び忘れて null のまま
register 内で mailer.send(...) を呼んで NullPointerException

という事故が起きます。
ここが初心者には一番危険なポイントです。

結論として、

絶対に必要な依存はコンストラクタインジェクション
あってもなくても動くオプション機能はセッターインジェクション

という使い分けを強くおすすめします。


セッターインジェクションが“良い選択”になる場面

オプション機能・後付け機能を注入したいとき

例えば、「ログを出せたら出すが、なくても動く」という場合です。

interface Logger {
    void log(String message);
}

final class Service {

    private Logger logger;     // なくても動く

    void setLogger(Logger logger) {
        this.logger = logger;
    }

    void doWork() {
        if (logger != null) {          // あれば使う
            logger.log("start");
        }
        // 実際の処理
    }
}
Java

この例では、ログがなくても Service 自体は動作できます。
このような「存在しなくても壊れない依存」は、セッターインジェクションが向いています。

本番では logger を注入し、
テストや簡易ツールでは logger を設定しない、
といった柔軟な使い方ができます。

フレームワークが後からセットしてくるケース

DI コンテナやフレームワークでは、アノテーションを付けることでセッターインジェクションを行う場合があります。

イメージとしては次のようなものです。

class SomeController {

    private UserService userService;

    @Autowired
    void setUserService(UserService userService) {   // フレームワークがここで注入
        this.userService = userService;
    }
}
Java

自分で new するのではなく、
フレームワーク側がアプリケーション起動時に setter を呼んでくれるパターンです。

このような「ライフサイクルの都合であとから注入したい」ケースでも、セッターインジェクションが使われます。


セッターインジェクションのメリットとデメリット(深掘り)

メリット

セッターインジェクションのメリットは、以下のようなところにあります。

まず、オプション依存を柔軟に扱えること。
設定ファイルや環境によって「あるときは設定」「ないときは null のまま」といった状態を作りやすいです。

また、初期化順がややこしいとき、「まずオブジェクトを作ってから依存を差し込む」といった制御がしやすいです。
特にフレームワークがライフサイクルを管理している場合、コンストラクタで全部そろわない場合に使われます。

さらに、テストコードで「一時的に差し替えたい」ときも楽です。
コンストラクタで注入したものを、テスト中だけ setter で別のものに変える、といった使い方も理論上できます。

デメリット(ここが重要)

一方で、セッターインジェクションには強めのデメリットもあります。

一つ目は、「設定忘れ」がコンパイルでは分からないことです。
コンストラクタインジェクションなら「引数を渡していない」ことでコンパイルエラーになりますが、
セッターインジェクションは、呼び忘れていてもコンパイルは通ります。
その結果、実行時に NullPointerException で初めて気付く、という事故が起こります。

二つ目は、フィールドを final にできないことです。
セッターで後から代入するので、private final にはできません。
途中で別のオブジェクトに差し替えられてしまう可能性が生まれます。

三つ目は、「本当に必須か任意か」がコードだけでは分かりにくくなりがちなことです。
セッターで注入していると、「これはセットされている前提なのか、あってもなくてもいいものなのか」が曖昧になります。

このあたりを理解しておくと、
「何でもかんでもセッターインジェクションにする」のが危険だと気付けるはずです。


コンストラクタインジェクションと組み合わせて使う

基本はコンストラクタ、補助的にセッター

実務的な設計では、次のような方針が筋が良いです。

「ないとクラスが動かないもの」は、全部コンストラクタインジェクション。
「あれば便利だけど、なくてもとりあえず動くもの」は、セッターインジェクション。

例えば、次のようなクラスを考えます。

interface Payment {
    void pay(int amount);
}

interface Logger {
    void log(String message);
}
Java

これに対するサービスを設計するとしたら、こんなイメージです。

final class OrderService {

    private final Payment payment;    // 必須
    private Logger logger;            // 任意

    OrderService(Payment payment) {   // コンストラクタインジェクション
        this.payment = payment;
    }

    void setLogger(Logger logger) {   // セッターインジェクション
        this.logger = logger;
    }

    void place(int amount) {
        if (logger != null) logger.log("start");
        payment.pay(amount);
        if (logger != null) logger.log("done");
    }
}
Java

支払い機能(Payment)はないと注文そのものが成立しないのでコンストラクタで注入。
ログ機能(Logger)は、あればログを出すが、なくても「注文を通す」という本質的な処理はできます。
そのため、セッターインジェクションにしています。

こうやって「役割の重要度」で注入方法を使い分けると、コードの意図がとてもクリアになります。


まとめ:セッターインジェクションを安全に使うために

セッターインジェクションは、

「依存をあとから差し込む」
「オプション機能や環境依存の設定を柔軟に変える」

ための便利な手段です。

ただし、

必須の依存に使うと、設定忘れからの NullPointerException を生みやすい
フィールドを final にできず、クラスの不変性が弱まる

という弱点があるので、乱用は禁物です。

実務では、

基本はコンストラクタインジェクション
どうしても「あとから設定したい」「任意にしたい」部分だけセッターインジェクション

というスタンスが、安全で分かりやすいです。

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