Spring Frameworkに限らず、Javaのフレームワークといえば、この「DI - 依存性の注入」というものが用意されていて、このDIを使用することが前提になっています。
このDI、みなさん理解していますか?
- フレームワークの作法だから、なんとなくDIを使ってる。
- DIを使うことのメリットがわからない。
- newすればいいんじゃないの?
こんな風に思っている方、いませんか?
語りつくされている感はありますが、あらためて、DIのメリットについて難しい話は抜きで考えてみましょう。
「依存性の注入」ってなに?
DIは「依存性の注入」と説明されることが多いです。耳タコです。
では、「依存性の注入ってなに?」と聞かれて、答えることができますか?意外と答えられない人が多いと思います。
Javaの用語を無理やり日本語にして、逆にわかりにくくなるケースです。マツキは、この「DI = 依存性の注入」と「Serialize = 直列化」が特にわかりにくい日本語だと思ってます。英語のままでいいじゃん!!
では、分かりやすく、「依存性」と「注入」をそれぞれ理解していきましょう。
「依存性」ってなに?
これは、あるプログラムが別のプログラムに依存していることを表しています。
分かりやすく、具体例で説明しましょう。
DIを使うとき、interfaceクラスを定義して、そのインターフェースクラスの実装クラスを実装することが多いと思います。理由は後で説明しますが、この作法どおり、インターフェースクラスを作ります。
public interface 乗り物 { double 距離から時間を取得(double 距離); }
そして、実装クラスを実装します。
public class 車 implements 乗り物 { private static int 時速 = 40; @Override public double 距離から時間を取得(double 距離) { return 距離 / 時速; } }
作成したインターフェースクラス「乗り物」を使用する「時間」クラスを作ります。
public class 時間 { public double 乗り物でかかる時間(double 距離) { 乗り物 使う乗り物 = new 車(); double かかる時間 = 使う乗り物.距離から時間を取得(距離); return かかる時間; } }
DIを使用していない、普通のJavaプログラムです。
ポイントは以下の一行に集約されています。
乗り物 使う乗り物 = new 車();
「乗り物」インターフェースで変数を宣言して、「車」実装クラスのインスタンスを代入しています。
java.util.Listを使うときの作法と同じですね。
List list = new ArrayList();
インターフェースクラスで変数を宣言して、実装クラスのインスタンスを代入します。
これは、実装を簡単に切り替えることができるようにする、デザインパターンになります。
例えば、「乗り物」インターフェースを実装した「自転車」実装クラスを作って、インスタンスの代入を次のように変えるだけで、「車」から「自転車」に変更することができます。
乗り物 使う乗り物 = new 自転車();
このインスタンス生成部分以外は、「乗り物」が「車」なのか「自転車」なのかは意識する必要がないのです。
では、「依存性」に話を戻します。この「時間」クラスは「乗り物」インターフェースを使って、距離から時間を求めています。なので、「時間」クラスは「乗り物」インターフェースに依存しています。そして、「車」実装クラスのインスタンを生成しているので、「車」実装クラスにも依存しています。
「時間」クラスは「乗り物」インターフェースと「車」実装クラスに依存しているのです。これが依存性です。
「注入」ってなに?
「依存性の注入」の「注入」は、「インスタンスの代入」と考えてよいです。先ほどの「時間」クラスをSpringのDI風に書き換えると次のようになります。
public class 時間 { @Autowired private 乗り物 使う乗り物 ; public double 乗り物でかかる時間(double 距離) { double かかる時間 = 使う乗り物.距離から時間を取得(距離); return かかる時間; } }
「乗り物」インターフェースのフィールドを宣言して、@Autowiredアノテーションを指定しています。
@Autowiredアノテーションを指定すると、DIの仕組みで、そのフィールドに実装インスタンスが代入されるのです。
つまりは、このインスタンスの代入が注入なのです。
代入される実装のほうは、そのクラスのインスタンスがDIで管理されるよう、@Componentアノテーションを指定します。
@Component public class 車 implements 乗り物 {
DIのメリット
本題のDIを使った時のメリットです。
依存を減らせる
「時間」クラスの依存性をDIを使う前と使った後で比較してみましょう。
DIを使う前は、「乗り物」インターフェースと「車」実装に依存していました。DIを使うことで、実装への依存がなくなり、「乗り物」インターフェースへの依存のみになりました。
依存性が下がると、依存先の変更の影響を受けにくくなることがメリットですが、もともとインターフェースクラスを利用しているので、DIを使用したからといっても依存先の変更の影響を受けにくくなったわけではありません。
依存を減らしたメリットは、この後に説明する「実装の切り替えが容易になる」につながるメリットです。
実装の切り替えが容易になる
実装乗り切り替えが容易となるよう、インターフェースを使用していますが、DIを使用すると、さらに実装の切り替えが容易になります。 例えば、テスト時はスタブ実装に切り替えたい、ということがあると思います。DIを使わない場合、テスト時だけnew実装クラスを切り替えるコードを書く必要があります。
環境変数「profile」の内容によって実装を切り替えるのであれば、以下のようなコードになります。
乗り物 使う乗り物; if (System.getenv("profile").equals("test")) { 使う乗り物 = new テスト用の乗り物(); } else { 使う乗り物 = new 車(); }
このようなコードを切り替えが必要なすべての箇所に実装する必要がありますし、プログラム中にテストのためのロジックが入り込むのは良くありません。
Spring FrameworkのDIでは、実装クラスを容易に切り替える仕組みが用意されています。
DIする側は、普通にDIするコードを書くだけです。
@Autowired private 乗り物 使う乗り物 ;
DIされる実装が複数あった場合に、どれがDIされるかを決定するための定義が必要になります。それぞれの実装に@Profileアノテーションでプロファイル名を定義します。
@Component @Profile("default") public class 車 implements 乗り物 {
@Component @Profile("test") public class テスト用の乗り物 implements 乗り物 {
後はプログラムの実行時に環境変数や、プロパティファイルに定義されたspring.profiles.activeの内容によって、DIされる実装クラスが切り替わります。
この仕組みを使うと、実装の切り替えロジックを実装する必要はなくなりますし、複数のインターフェースの実装を一括して切り替えることが容易になります。
オブジェクトのライスサイクルの管理をDIがやってくれる
オブジェクトのライフサイクルとは、そのオブジェクトが生成されてから破棄されるまでのことです。
「そんなのnewしてから、参照が外れてGCされるまで」と答える人もいるでしょう。間違いではありませんが、ライフサイクルをプログラムで制御するケースもあります。
分かりやすいものでは、シングルトンパターンのオブジェクトです。オブジェクトが必要になったとき、newして新しいインスタンスを生成するのではなく、あらかじめ生成されている一つのインスタンスを共有しよう。というものです。
このシングルトンパターンのライフサイクルを使用する場合、シングルトンとなるクラスでnewを禁止し、getInstanceのようなシングルトンのインスタンスを取得するメソッドを用意する必要があります。そして、シングルトンとなるクラスを利用する側も、newでインスタンスを生成するのではなく、getInstanceメソッドを呼び出してインスタンスを取得するように実装します。
DIを使用すると、オブジェクトのライフサイクルを管理してくれますし、ライフサイクルに関する実装を省略することができます。
DIする側は、普通にDIするコードを書くだけです。
@Autowired private 乗り物 使う乗り物 ;
DIされる側の実装は、ライフサイクルに関する実装は不要で、そのクラスのオブジェクトのライフサイクルを定義するだけです。
このライフサイクルのことを「スコープ」と言い、Spring Frameworkでは、スコープの定義を省略すると、シングルトンになります。(明示的にシングルトンであることを定義することもできます)
Springでは、@Scopeアノテーションでスコープを定義します。
シングルトンスコープであれば、次のようになります。
@Component @Scope("singleton") public class テスト用の乗り物 implements 乗り物 {
DIのたびに新しいインスタンスを生成(new)するのであれば、次のようになります。
@Component @Scope("prototype") public class テスト用の乗り物 implements 乗り物 {
DIがライフサイクルの管理をしてくれるので、ライフサイクルに応じた実装が不要であることがわかります。
ライフサイクルの実装が不要になり、かつ、ライフサイクルの変更が容易になることがメリットとなります。
まとめ
以上がDIの利点です。
最後に簡単にメリットのまとめとユースケースをまとめます。
実装の切り替えが容易になる
実行環境やテスト時など、一部のロジックの切り替えが容易になります。
例えば、月末に特殊な処理が動くようなロジックをテストしようとすると、月末まで待つか、PCの日付を変更する必要がありますが、日付の取得をロジックに切り出し、そのロジックを切り替えることで特定の日付を常に返すことが可能になります。
ライフサイクルの管理をしてくれる
デフォルトでは、DI管理されているインスタンスはシングルトンになります。これは無用なメモリ消費やインスタンスを都度、生成する実行コストを削減する効果があります。
半面、シングルトンでは状態を持つことができません。クラスにインスタンスのフィールドを定義しすると、そのフィールドもすべての処理で共有されてしまうためです。
開発をしていて、インスタンスに状態を持たせる必要が出たときに、そのクラスのスコープを変更するだけで、ライフサイクルが切り替わるので、最小のプログラム変更でスコープを変更することができます。