読者です 読者をやめる 読者になる 読者になる

SpringでField InjectionよりConstructor Injectionが推奨される理由

SpringでField InjectionよりConstructor Injectionが推奨される理由を調べてみたメモです。


(2016/12/30) サンプルコードにfinalをつけるように修正
(2017/03/29) Immutabilityについて追記
---

家でも会社でもIntelliJを使って開発しているのですが、
Spring Bootで@Autowired(@Inject)を使うと下記のような警告が出るようになりました。

f:id:pppurple:20161229225928p:plain

警告内容を見てみると、フィールドインジェクションは推奨されません、とのこと。
「Field injection is not recommended.」
f:id:pppurple:20161229225944p:plain


警告の詳細を見てみると下記のように書いてあります。
「Field injection is not recommended.
 Spring Team recommends: "Always use constructor based dependency injection in your beans.
 Always use assertions for mandatory dependencies."」
f:id:pppurple:20161229225952p:plain

つまり、Spring Teamは、beanのインジェクションには常にコンストラクタインジェクションを使うように、と推奨しています。



自分は今までずっとフィールド・インジェクションを使用してきました。
理由は、

  • 記述が簡潔なこと
  • 依存関係の追加、削除も簡単なこと
  • 特に問題が発生したことが無いこと
  • コンストラクタ・インジェクションを推奨する理由が分からないこと

などです。

ということでフィールド・インジェクションではなくコンストラクタ・インジェクションが推奨される理由を調べて見ました。
うまく日本語記事が発見できなかったのですが、英語の記事はたくさんあったので、
自分なりにまとめてみたら、だいたい理解することができました。

SpringでのDependency Injection

それぞれコンストラクタ・インジェクション、フィールド・インジェクション、セッター・インジェクション
を使用した簡単な例です。

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

(Spring 4.3からは単一のコンストラクタの場合、@Autowired不要になります)

ConstructorInjection.java

@Component
public class ConstructorInjection {
    private final MyServiceA myServiceA;
    private final MyServiceB myServiceB;

    @Autowired
    public ConstructorInjection(MyServiceA myServiceA, MyServiceB myServiceB) {
        this.myServiceA = myServiceA;
        this.myServiceB = myServiceB;
    }
}
フィールド・インジェクション

FieldInjection.java

@Component
public class FieldInjection {
    @Autowired
    private MyServiceA myServiceA;
    @Autowired
    private MyServiceB myServiceB;
}
セッター・インジェクション

SetterInjection.java

@Component
public class SetterInjection {
    private MyServiceA myServiceA;
    private MyServiceB myServiceB;

    @Autowired
    public void setMyServiceA(MyServiceA myServiceA) {
        this.myServiceA = myServiceA;
    }

    @Autowired
    public void setMyServiceB(MyServiceB myServiceB) {
        this.myServiceB = myServiceB;
    }
}


上の例を見てわかる通り、フィールド・インジェクションはすごく簡潔に書けます。
コード量も圧倒的に少ないです。
新しい依存関係を追加する場合も、フィールドにクラス名を追加して@Autowiredをつけるだけなのでとても簡単です。


例えば下記の様に多くの依存関係を持ったクラスの場合、
コンストラクタ・インジェクションだと記述が大変になると思ってました。
でも調べていくとその考えがそもそも間違っていることが分かりました。

HasManyDependencies.java

@Component
public class HasManyDependencies {
    private MyServiceA myServiceA;
    private MyServiceB myServiceB;
    private MyServiceC myServiceC;
    private MyServiceD myServiceD;
    private MyServiceE myServiceE;
             :
             :

    @Autowired
    public HasManyDependencies(MyServiceA myServiceA, MyServiceB myServiceB, MyServiceC myServiceC
                                 , MyServiceD myServiceD , MyServiceE myServiceE, …… ) {
        this.myServiceA = myServiceA;
        this.myServiceB = myServiceB;
        this.myServiceC = myServiceC;
        this.myServiceD = myServiceD;
        this.myServiceE = myServiceE;
                :
                :
    }
}

コンストラクタ・インジェクションが推奨される観点

コンストラクタ・インジェクションが推奨される観点を下記にまとめてみました。

単一責任の原則

コンストラクタ・インジェクションが煩雑に感じる場合、クラスに多くの依存関係があることを知らせてくれています。
依存関係が多い場合、そのクラスが多くの責任を持ちすぎてる可能性があります。これは単一責任の原則に違反します。
コンストラクタ・インジェクションを使用することで気付きやすくなります。
(上の例の場合などがそうです)

依存関係の明示

そのクラスの必須の依存関係が何なのか明確になります。
必須の依存関係の場合コンストラクタ・インジェクションを使用し、
オプションの依存関係の場合、セッター・インジェクションを使用することで、
その依存関係が必須かオプションが明確に区別できます。

再利用性(テスタビリティ)

DIコンテナで管理されるクラスは、特定のDIコンテナに依存せず、POJOであるべきです。
そうすることで、DIコンテナを使用せずにインスタンス化して単体テストが可能となり、
また、別のDIフレームワークに切り替えることも可能になります。

しかし、フィールド・インジェクションを使用している場合、
そのクラスで必要な依存関係をインスタンス化する方法がありません(リフレクションを除き)。
そのクラスをインスタンス化し、依存関係のクラスを使用とするとNullPointerExceptionが発生してしまいます。
つまり、DIコンテナ以外では再利用できないことになります。

コンストラクタ・インジェクションを使用することで、インスタンス化する際に必要な依存関係を強制することが出来ます。

Immutability(不変性)

コンストラクタ・インジェクションでは、フィールドはfinalとして宣言できます。
そのため、イミュータブルなオブジェクトにしたり、必要な依存関係だけ不変にすることが出来ます。
フィールド・インジェクションの場合、フィールドはfinalでは宣言できないので依存関係は変更可能なままです。

循環依存性

コンストラクタ・インジェクションを使用する場合、循環依存が問題になる場合があります。
これはAクラスがBクラスをインジェクト、BクラスがCクラスをインジェクト、CクラスがAクラスをインジェクトする様な場合、
BeanCurrentlyInCreationExceptionが発生します。
循環依存を使用したい場合はコンストラクタ・インジェクションは使用できません。
しかし、循環依存はアンチパターンとされているため、クラス設計・分離が間違っている可能性を示してくれます。

Lombokを使用してコンストラクタ・インジェクションを簡単に書く

Lombokを使用してコンストラクタ・インジェクションを簡単に書くことができます。
しかし、依存関係の明確さは薄れてしまう気がします。


(Spring4.3以降)
Spring4.3以降ではコンストラクタ・インジェクションで@Autowiredを省略できます。
@RequiredArgsConstructorは初期化されていないfinalなフィールドをパラメータに取るコンストラクタを生成します。
@NonNullが付与されたフィールドはnullチェックが実行され、パラメータがnullの場合はNullPointerExceptionが投げられます。

@RequiredArgsConstructor
@Component
public class ConstructorInjectionWithLombok {
    @NonNull
    private final MyServiceA myServiceA;
    @NonNull
    private final MyServiceB myServiceB;
}

(Spring4.2以前)
Spring4.2以前はコンストラクタ・インジェクションで@Autowiredを省略できないので、
(onConstructor = @__(@Autowired))を追加する必要があります。

@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Component
public class ConstructorInjectionWithLombok {
    @NonNull
    private final MyServiceA myServiceA;
    @NonNull
    private final MyServiceB myServiceB;
}


という訳で、フィールド・インジェクションが推奨されない理由、コンストラクタ・インジェクションが推奨される理由が
自分なりにわかったので、今後はコンストラクタ・インジェクションを使用していきたいと思います。


【参考】
こちらの記事を参照しました。
多くの議論が書かれているので是非読んでみて下さい。


@makingさんに指摘してもらったので修正しました。


終わり。

サンプルコードは下記に置きました。
github.com