本記事ではSpring Bootの特徴の一つであるDI(Dependency Injection:依存性注入)を使い、Serviceクラスを実装してみます。
ソースコード自体はものすごく単純なのですが、概念の理解が難しいと思います。なるべく丁寧に説明してみますが、理解できなくてもとりあえずSpring Bootではこんな感じで開発していくんだと何となく理解してください。
DIのメリットは規模が大きくならないと理解しにくいので、現段階では完全に意義を理解できなくても大丈夫です。
この記事はSpring Boot入門:Formと入力チェック(バリデーション)の続きとなります。
この記事のソースコード
この記事のソースコードはGithubに公開しています。
GithubからSpring BootプロジェクトをEclipseにインポートする方法は次の記事を参考にしてください。
Spring Bootプロジェクトを作成
Spring Bootプロジェクトを作成します。適当なプロジェクト名やパッケージを設定しましょう。
特に注意点はありません。いつもの通りです。
そもそも DI(Dependency Injection:依存性注入) とは何か
実装をする前に、そもそもDIとは何かを説明します。
私の理解では、「DIとは直接インスタンスを生成しないで、外部から与えること」です。
Spring Bootでは特定のアノテーションを付けたクラスのインスタンスをシステムが生成して、必要に応じで他のインスタンスに渡す形で実装します。
広い意味では、メソッドの引数として何らかの操作を行うクラスのインスタンスを渡すこともDIということになります。ラムダ式を渡すのもDIの一種です。
DI(Dependency Injection:依存性注入)の実装
Spring BootでDIを実装してみましょう。今回のサンプルではメリットがわからないと思いますが、とりあえずこんな感じで実装するんだということを見てください。
ControllerでServiceをIDする
Spring BootのアプリケーションはControllerが入り口になることが多いので、このクラスでのDIから見ていきます。
package blog.tsuchiya.tutorial.step5.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import blog.tsuchiya.tutorial.step5.service.MainService;
@Controller
public class MainController {
private static final String TEXT = "テスト対象文字列";
/**
* 今回DIする対象。コンストラクタでSpring Bootが自動的に代入してくれる。
* Qualifierアノテーションで対象のクラスを特定している。
* 今回はMainServiceを実装したコンクリートクラスが2つあるのでこの
* アノテーションが必要になった。
* MainServiceを実装したクラスが1つしかなかったらQualifierは不要。
* フィールドにAutowiredアノテーションを付けるフィールドインジェクションは
* 非推奨なのだが、今回は練習のためあえて利用した。
* 公式サイトではコンストラクタインジェクションが推奨されている。
*/
@Autowired
@Qualifier("mainServiceImpl1")
private MainService service1;
/**
* 同じくDI対象を指定。
*/
@Autowired
@Qualifier("mainServiceImpl2")
private MainService service2;
@GetMapping("/")
public String index(Model model) {
model.addAttribute("value1", this.service1.decorate(TEXT));
model.addAttribute("value2", this.service2.decorate(TEXT));
return "index";
}
}
実際にDIを行うのはこの部分です。
@Autowired
@Qualifier("mainServiceImpl1")
private MainService service1;
コメントにも書いたとおり、フィールドに@Autowiredをつけるフィールドインジェクションは非推奨です。しかし、lombokの@RequiredArgsConstructorでコンストラクタを自動で実装する場合、デフォルトだと@Qualifierがちゃんと機能しないのでこのクラスではこの様な形になっています。
さて、Sprign Bootが管理するクラスのインスタンス(@Controller、@Service、@Componentなどのアノテーションを付けたクラス)のフィールドに@Autowiredアノテーションを付けると、Spring Bootが自動的に対象クラスのインスタンスをこのフィールドに格納してくれます。
格納されるのはフィールドの型に指定されたクラスのインスタンスか、そのサブクラスです。今回の例ではインタフェースMainServiceを実装したクラスが2つあるので、明示的にどのクラスを格納するか指定するために@Qualifierアノテーションを使っています。
@Qualifierに指定するのは、デフォルトだとクラス名の先頭を小文字にした文字列です。
ちなみに、@AutowiredできるのはSpring Bootが管理しているインスタンスだけです。すなわち、@Component、@Service、@Repositoryなどのアノテーションを付けたクラスのインスタンスだけがDI可能となります。
DI(Dependency Injection:依存性注入)されるServiceクラス
MainControllerでDIされる方のクラスです。どちらもMainServiceインタフェースを実装しています。
一般的にServiceクラスにはビジネスロジックを書きます。Controllerに書いてしまうこともできるのですが、1つのクラスに仕事を集中させると後々管理が大変だったり修正しにくかったりするので、役割の分担をするわけです。
インタフェースは特に何も言うことはありません。必要なアノテーションも特になしです。
package blog.tsuchiya.tutorial.step5.service;
public interface MainService {
/**
* 文字列を装飾する
*
* @param target 装飾対象の文字列
* @return 装飾された文字列
*/
String decorate(String target) ;
}
設定が必要になるのはDIされるコンクリートクラスの方です。
package blog.tsuchiya.tutorial.step5.service;
import java.util.Objects;
import org.springframework.stereotype.Service;
@Service
public class MainServiceImpl1 implements MainService {
private static final String DECORATOR = "*";
/**
* 引数の文字列の前後に*をつける。
* 引数がnullだった場合は空の文字列を返す
*/
@Override
public String decorate(String target) {
if(Objects.isNull(target)) {
return "";
}
return DECORATOR + target + DECORATOR;
}
}
とは言っても、そんなに難しいことはありません。
- MainService型のフィールドにDIするのでMainServiceを実装する
- Spring Bootでインスタンスを管理するため、@Serviceアノテーションをつける
注意点はこの2つだけです。
また、ID対象となるクラス内で更にDIすることもできます。
package blog.tsuchiya.tutorial.step5.service;
import java.util.Objects;
import org.springframework.stereotype.Service;
import blog.tsuchiya.tutorial.step5.component.StringCounter;
import lombok.RequiredArgsConstructor;
@Service
// finalなフィールドを引数に取るコンストラクタを自動で生成するアノテーション
@RequiredArgsConstructor
public class MainServiceImpl2 implements MainService {
/** DI対象のクラス内で更にDIすることも可能 */
private final StringCounter counter;
/**
* 文字列の後に文字列の長さを付け加える。
* nullの場合は空文字列を返す
*
* @param target 装飾対象の文字列
* @return targetに数値を付け加えた文字列
*/
@Override
public String decorate(String target) {
if(Objects.isNull(target)) {
return "";
}
return target + counter.length(target);
}
}
MainServiceImpl2ではコンストラクタインジェクションを行っています。lombokの@RequiredArgsConstructorアノテーションでfinalなフィールドを引数に持つコンストラクタを自動生成していますが、こうすることでフィールドに対応クラスのインスタンスがDIされるようになります。
DI対象のクラスは以下の通り。
package blog.tsuchiya.tutorial.step5.component;
import java.util.Objects;
import org.springframework.stereotype.Component;
/**
* ComponentとしてSpring Bootに登録するクラス。
* DI対象のクラスに更にDIできることを示すためだけに作成。
*/
@Component
public class StringCounter {
/**
* 引数の文字列の長さを返す。nullの場合は長さ0を返す
* @param target 長さを調べる文字列
* @return
*/
public int length(String target) {
if(Objects.isNull(target)) {
return 0;
}
return target.length();
}
}
こちらもDI対象にするためにアノテーションをつけていますが、@Serviceではなく@Componentを使いました。
実は機能的には@Serviceと@Componentに差はありません。一応、ビジネスロジックを書くクラスでは@Serviceを、ユーティリティクラスのような機能を提供するクラスには@Componentを利用するという慣例みたいなものがあるだけです。
出力先のThymeleafテンプレート
一応Thymeleafも貼り付けておきます。単純に文字列を表示しているだけで説明するような内容はありません。
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>結果出力ページ</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body>
<p class="text-primary" th:text="${value1}"></p>
<p class="text-secondary" th:text="${value2}"></p>
</body>
</html>
なぜDI(Dependency Injection:依存性注入)が必要か
今回の例では、別にDIを行わなくても特に苦労することなく実装が可能です。
DIするのではなく、必要なときにコンストラクタを使ってインスタンスを生成しても困ることはないでしょう。
にもかかわらず、DIという機能があるのが以下のような理由があるからです。
- クラス間の結合を疎結合にする
- 単体テストを行いやすくする
- ロジックの切り替えが楽になる
- インスタンスのライフサイクルをSpring Bootが行ってくれる
クラス間の結合を疎結合にする
各クラス間の結合は、疎であればあるほどいいということになっています。なぜかといいますと、変更に対して強くなるからです。
例えば、DIを使わずクラス内でコンストラクタを使っていたとします。その場合、コンストラクタの引数を変更すると当然コンストラクタを使っているクラスでも修正が必要になります。
一方、DIを行っているのであれば特に変更は必要ありません。
DIを行うことで、変更に強くなっているわけです。
単体テストを行いやすくなる
ちょっと現段階では説明が難しいですが、単体テストも行いやすくなります。
上で書いたとおりクラス間の結合が疎になっているため、特定のクラスのテストもやりやすくなるんだと理解してください。
また、DIの対象を切り替えることができるので、実際のコンクリートクラスの代わりにモック(シグネチャは同じだけど実装がないクラス。テストで使うことがある)をDIすることもできます。
ロジックの切り替えが楽になる
DIの対象を切り替えるのは簡単なため、ロジックの切り替えも楽になります。
例えば何か新しいMainServiceを作った場合、@Qualifierの引数を変更するだけで利用するMainServiceを切り替えることができます。
インスタンスのライフサイクルをSpring Bootが行ってくれる
今回は説明していませんが、DIを使うとライフサイクルの管理もSpring Bootがしてくれます。
@Scopeというアノテーションを使うことで、ずっと存在し続けるsingleton(デフォルトはこれ、したがって今回すべてのServiceとComponentはおなじJVM上で1つだけ)から、毎回異なるインスタンスを生成するprototypeまでを設定可能です。
私がこれまで実装したシステムではほとんどデフォルトのsingletonしか使いませんでしたが、システム要件によってはライフサイクルの管理ができると便利な場合もあるでしょう。
まとめ:とりあえず作法だと思ってDIを利用してみる
DIの使い方とそのメリットを説明してきました。
正直、入門レベルではDIの何が嬉しいのかは理解が難しいと思います。こんなことしなくても、全部Controllerクラスに書いてしまっても同じことができるしそれで問題も起きないでしょうから。
それでも、今の段階からControllerとServiceで機能を切り分け、DIを使うことは無駄になりません。将来規模の大きな開発を行うことになると、必ず必要になる考え方だからです。
少なくとも、私が参画したプロジェクトでDIを使わないものはありませんでした。(Spring Bootに限らず、他のフレームワークでもDIを使いました)
今は面倒だと思っても、将来のためにこういう機能があることを記憶しておき、積極的に使うようにしてください。
サンプルコードはGithubに公開しています。
質問がありましたらTwitterやお問い合わせフォームからどうぞ。できる限り対応したいと思っています。
Spring Boot入門:Spring Data JPAでデータベース操作に続きます。
コメント