はじめに
転職先でJavaを業務で使うことになり、2週間前から本格的にJava, Spring Bootの勉強を始めました。
この土日を使ってDIやシングルトンを中心に学んだため、頭の整理も兼ねてまとめたいと思います。
DIとは
DI (Dependency Injection / 依存性の注入)のこと。
- オブジェクト指向におけるSOLID原則の一つに依存性逆転の法則があり、それを実現する方法がDI。
- クラス間を密結合にせず、疎結合を保つことで開発における高い保守性と変更によるテストへの影響を抑えることができる。
DIしていない例
実行クラスが他クラスから情報を取る場合は、newすることで他クラスの情報を取得する
- 実行クラスのメソッド内で別のクラスのインスタンスをnewして生成している
- 実行クラスのメソッドの引数として他のクラスのインスタンスを使用している
public class B {
private String hoge;
public String getHoge() {
return this.hoge;
}
public void setHoge(String fuga) {
this.hoge = fuga;
}
}
public class A {
public static void main(String[] args) {
B b = new B();
b.setHoge("piyo");
System.out.println(b.getHoge());
}
}
クラスAはmainメソッド内でBをインスタンス化しており、A has Bの関係であるといえます。
これを例えば、以下のようにBを変更します。
public class B {
private int hoge; // 変更箇所
public int getHoge() { // 変更箇所
return this.hoge;
}
public void setHoge(int fuga) { // 変更箇所
this.hoge = fuga;
}
}
変数hogeの型をStringからintに変更しました。
それによって、Bを呼び出してるAも変更する必要が出てきてしまいました。
public class A {
public static void main(String[] args) {
B b = new B();
b.setHoge(100); // 変更箇所
System.out.println(b.getHoge());
}
}
つまりこの状態を密結合の状態であり、AクラスはBクラスに依存しています。
これを解決するためにインターフェースを使うことで依存関係を逆転します。
// インターフェースの定義
public interface InterfaceClass {
public abstract String getHoge();
public abstract void setHoge(String hoge);
}
// DI用クラス
public class B implements InterfaceClass {
private String hoge;
@Override
public String getHoge() {
return this.hoge;
}
@Override
public void setHoge(String fuga){
this.hoge = fuga;
}
}
// 実行クラス
public class A {
public static void main(String[] args) {
InterfaceClass nonDependency = new B();
nonDependency.setHoge("piyo");
System.out.println(nonDependency.getHoge());
}
}
InterfaceClassを新しく定義し、AクラスではBクラスをインスタンス化する際に型をInterfaceClass型で生成しました。
Bクラスの具象メソッド(Setter Getter)は、先にインターフェースで定義された同名のメソッドをオーバーライドしています。
これで先ほどのように変数hogeの型をStringからintに変換します。
// インターフェースの定義
public interface InterfaceClass {
public abstract int getHoge(); // 変更箇所
public abstract void setHoge(int hoge); // 変更箇所
}
// インターフェースの定義
// DI用クラス
public class B implements InterfaceClass {
private int hoge; // 変更箇所
@Override
public int getHoge() { // 変更箇所
return this.hoge;
}
@Override
public void setHoge(int fuga){ // 変更箇所
this.hoge = fuga;
}
}
// 実行クラス (変更なし)
public class A {
public static void main(String[] args) {
InterfaceClass nonDependency = new B();
nonDependency.setHoge("piyo");
System.out.println(nonDependency.getHoge());
}
}
インターフェースを挟むことで、Bクラスの変更が入った場合でも、Aクラスには何も影響を与えていないことがわかります。
むしろBクラスはインターフェースをimplementしているため、依存関係はBクラスはInterfaceClassクラスに依存していると言えます。
インターフェースを挟む前は、Bクラスの依存関係はAクラスだけなのか不透明でしたが、インターフェースを挟むことで、Bクラスに変更が入った場合、直すポイントはインターフェースのみに限定されるようになりました。
これをなぜDI(依存性の注入)と呼ぶのかしっくり来ませんでしたが、DIはオブジェクトの注入だと再翻訳してる方も多くおり、インターフェースにオブジェクトを注入していると考えるととてもしっくり来ることができました。
ここで残ってる問題として、実際のプログラムではBを呼び出すのはAだけとは限らず、Bを呼び出すたびにBのインスタンスが増え続けてしまう可能性があることです。
これを解決するのがシングルトンパターンです。
シングルトンパターンとは
シングルトンパターンとは、オブジェクト指向におけるデザインパターンの一つです。
- クラスのインスタンスを1つしか作らないことを保証する。
- privateのコンストラクタを作成する。
- staticフィールドにインスタンスを作ることで、どのクラスから呼び出されても使い回すことができる。
Spring frameworkやSpring bootでもDIコンテナを使うことでシングルトンを実現しています。
よく聞く話で、そうなんだーくらいの知識は持っていましたが、実際にどう実現しているのかわからなかったため、この機会に確認してみます。
public class SingletonTest {
// コンストラクタはprivateなので外のクラスからは呼び出せない。
// そのためインスタンスを内部から作成し、それを定数として不変オブジェクトとしている
public static final SingletonTest INSTANCE = new SingletonTest();
private SingletonTest() { // コンストラクタ
System.out.println("唯一のインスタンスを生成しました");
}
}
public class getInstance {
public static void main(String[] args) {
// static変数INSTANCEをSingletonTest型のsiに代入している
SingletonFinalField si = SingletonFinalField.INSTANCE;
// SingletonTest()はprivateなので外のクラスからは呼び出せない。
// SingletonTest newInstance = new SingletonTest();
}
}
SingletonTestクラスでは、コンストラクタをprivateに設定し、外のクラスからインスタンス化するために呼び出しが行えないようになっています。
そのため、SingletonTestクラス内部から外のクラスから呼び出せるstaticの定数INSTANCEを定義し、自らのインスタンスを代入しています。
外からこのインスタンスを使いたい場合、このStatic定数を呼び出すことで使うことが可能になっているわけです。
え、なにこれすごい…
では、先程DIを実現したクラスをシングルトンパターンに変更します。
// インターフェースの定義
public interface InterfaceClass {
public abstract String getHoge();
public abstract void setHoge(String hoge);
}
// DI用クラス
public class B implements InterfaceClass {
private String hoge;
public static final B INSTANCE = new B();
private B() {} // プライベートのコンストラクタ
@Override
public String getHoge() {
return this.hoge;
}
@Override
public void setHoge(String fuga){
this.hoge = fuga;
}
}
// 実行クラス
public class A {
public static void main(String[] args) {
InterfaceClass nonDependency = SingletonFinalField.INSTANCE;
// InterfaceClass nonDependency = new B(); (newはしない)
nonDependency.setHoge("piyo");
System.out.println(nonDependency.getHoge());
}
}
これでシングルトンパターンでDIなものが出来上がりました!
話が少し脱線してしまいますが、上記では実行クラスAがBのstatic定数INSTANCEを直接呼び出してます。
インターフェースで更にStaticファクトリメソッドを定義することで、柔軟性があり可読性も損なわれないコードが書けるのではないかと思い、以下のようなものも作ってみました。
// 実行クラス (変更なし)
// インターフェースの定義
public interface InterfaceClass {
public abstract String getHoge();
public abstract void setHoge(String hoge);
static InterfaceClass newInstance(int val) { // Staticファクトリメソッド
System.out.println("インターフェースからStaticなファクトリメソッドを実行します。");
if ("a" == val) {
return B.getInstance();
} else if ("あ" == val) {
return C.getInstance();
} else {
return D.getInstance();
}
}
}
// DI用クラス
// 実際にはCクラスとDクラスも作成されている
public class B implements InterfaceClass {
private String hoge;
private static final B INSTANCE = new B(); // privateに変更
private B() {} // プライベートのコンストラクタ
public static B getInstance() { // 実行クラスからこのStaticメソッドを呼び出す
return INSTANCE;
}
@Override
public String getHoge() {
return "B: " + this.hoge;
}
@Override
public void setHoge(String fuga){
this.hoge = fuga;
}
}
// 実行クラス
public class A {
public static void main(String[] args) {
InterfaceClass nonDependency = InterfaceClass.newInstance("a"); //インターフェースのStaticファクトリメソッドを実行する
// InterfaceClass nonDependency = SingletonFinalField.INSTANCE;
// InterfaceClass nonDependency = new B();
nonDependency.setHoge("piyo");
System.out.println(nonDependency.getHoge());
}
}
最後は無理矢理感あったかな…と思いましたが、この土日調べたことの総まとめとしてすべての要素が入ったコードが完成したんじゃないかなって思います。
Spring等のフレームワークではアノテーションを用いてDI対象クラスのインスタンスをシングルトンになるよう管理しています。
// DI用クラス
@Component
// 以下のクラスのインスタンスをDIコンテナに格納する
public class InjectionClass implements InterfaceClass {
private String fuga;
@Override
public String getHoge() {
return this.fuga;
}
@Override
public void setHoge(String hoge){
this.fuga = hoge;
}
}
// mainクラス
public class Application {
// 上記でDIコンテナに格納したインスタンスをnewなしで使用出来る
@Autowired
InjectionClass injectionClass;
public static void main(String[] args) {
injectionClass.setHoge("piyo");
System.out.println(injectionClass.getHoge());
}
}
本当はアノテーションで省略されてるDIコンテナの処理がどのように行われてるのかもう少し勉強したかったのですが、今回はここで時間切れでした。
もっとJavaやSpringの理解を深めていきたい…!
土日2日で詰め込んだ内容ですので、理解が誤ってるところ等あればご指摘頂ければ大変勉強になるため、ぜひお願い致します…!