Dependency Injection とは
ソフトウェアの規模が大きくなると、さまざまなコンポーネントが互いに依存しあって動作するようになります。 Dependency Injection は、オブジェクトが他のオブジェクトに依存する場合に、その依存関係を管理するためのデザインパターンです。
このレクチャーでは Spring Framework が提供する Dependency Injection について学びましょう。 Spring は DI コンテナと呼ばれる仕組みによって、アプリケーション内の依存関係の管理を容易にします。 DI コンテナは、Bean と呼ばれるオブジェクトを管理し、必要なときにそれらをインスタンス化し、依存性を解決します。
目次
Dependency Injection とは?
Dependency Injection は、「依存性の注入」と訳されます。 「依存性の注入」とは「使いたいオブジェクト」を「直接 new せず外部から渡してもらうようにする」デザインパターンと言えます。 ここからは、「依存性」と「注入」に分けて詳細を説明していきます。
「依存性」とはなにか?
依存性とは、あるコンポーネントと別のあるコンポーネントが結びついている関係性のことです。
クラス A がクラス B のメソッドを呼び出しているとき、クラス A はクラス B に依存しているといいます。
たとえば以下のコードでは、TaskController
クラスは TaskService
クラスに依存しています。
TaskController#delete
メソッドで TaskService#delete
メソッドを呼び出しているからです。
@Controller
public class TaskController {
private final TaskService taskService;
@DeleteMapping("/tasks/{id}")
public String delete(@PathVariable("id") long id) {
taskService.delete(id);
return "redirect:/tasks";
}
}
「注入する」の意味
依存性を「注入」するという言葉は、「直接 new せず、外部からインスタンスを渡してもらう」という意味です。
DI を使わない場合、依存しているコンポーネントを直接初期化することになりますが、この方法では結合度が高くなり不都合が生じやすくなります。
たとえば、次のように前出の TaskService
を DI を使わずに直接 new するコードを考えてみましょう。
public class TaskController {
// DI を使わず、直接 new してインスタンスをセットする
private final TaskService taskService = new TaskService();
// ...
}
このコードでは、new
演算子を使って TaskService
を直接初期化しています。
現時点では TaskService
のコンストラクタに引数がないため特に負担はありません。
しかし、new TaskService(config, mapping, userService)
のように TaskService
のコンストラクタの引数が増えていくと、new での初期化が辛くなってきます。
Dependency Injection が必要な理由
DIはコードの柔軟性と保守性を高めるために必要です。具体的には、以下の点で重要です。
- 柔軟性と拡張性: DIを使うと、新しい機能の追加や既存機能の変更時に、コンポーネント同士の結びつきを緩めながら変更できます。
- テスト容易性: DIを利用すると、テスト時に依存コンポーネントを置き換えることが容易になり、テストコードを書きやすくなります。
- 可読性向上: DIは依存するコンポーネントがどこから来ているかを明示的に表示し、コードを理解しやすくします。
- コードの再利用: DIを活用すると、コンポーネントが直接依存するコンポーネントを生成せず、再利用可能なコードを最大限に活かせます。
このように、DIはコードの品質向上と保守性の向上を目指す際に不可欠な考え方です。
Spring での Dependency Injection
Spring で Dependency Injection をするには、以下の2つのコードが必要です。
- 登録: Bean を Application Context に登録する
- 取得: Bean を Application Context から取得する
Spring では、 DI 対象のクラスのことを Bean(ビーン)と呼びます。 また、Application Context とは Bean を管理するコンポーネントのことです。 「Bean を Application Context に登録する」「登録した Bean をApplication Context から取得して使う」というのが DI を使ったコードの処理の流れになります。
Bean の登録
Bean の登録は XML ベースの方法とアノテーションベースの方法がありますが、この講座ではアノテーションベースの方法を採用しています。 古いプロジェクトで XML ベースの既存のコードがあるなどの理由がない限りはアノテーションベースの方法を使うのが一般的です。
アノテーションベースの方法には、
- @Component アノテーションを使用する方法
- JavaConfig を使用する方法
の2つがあります。
@Component アノテーションを使用する方法
クラスに @Component
アノテーションを付与すると、そのクラスは Bean として Application Context に登録されます。
Spring Framework では、クラスの役割に応じて @Component
アノテーションを拡張した、特別なアノテーションが用意されています。
これらのアノテーションはコードの意味を明確にし適切なコンポーネントの種類を示すために使用されます。
以下がそれぞれのアノテーションとその役割です:
@Component
: 一般的なコンポーネントを示します。Springコンテナによって管理されるクラスを指定します。@Controller
: Spring MVCアプリケーション内のコントローラであることを示します。@Service
: ビジネスロジックを実行するサービスクラスであることを示します。@Repository
: データアクセス層のコンポーネントであることを示します。
たとえば、TaskController
クラスを Application Context に登録するときには以下のようなコードを書きます:
@Controller
public class TaskController {
// ...
}
JavaConfig を使用する方法
@Configuration
を付与したクラスで、@Bean
が付与されたメソッドの戻り値を Bean として登録する方法です。
前述の @Component
アノテーションとその仲間が使えないときに使われる方法です。
例えば、外部のライブラリが提供するクラスを利用するときなどにはこの方法で Bean 登録をします。
@Configuration
public class AppConfig {
@Bean
public ObjectMapper myBean() {
return new ObjectMapper();
}
}
Bean の取得
Bean を取得するには、その Bean を引数に取るコンストラクタを用意します。
次のコードでは TaskController
に TaskService
を引数に取るコンストラクタを用意しています。
Spring はこのコンストラクタを使って TaskController
を初期化します。
引数の TaskService には、Application Context に登録されている Bean が代入されます。
@Controller
public class TaskController {
private final TaskService taskService;
// Spring がこのコンストラクタを使って TaskController を初期化する
// 引数の TaskService には、Application Context に登録されている Bean が代入される
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
}
このコードを Lombok でリファクタリングしましょう。
Lombok の @RequiredArgsConstructor
をクラスに付与すると、final フィールドを引数に取るコンストラクタが生成できます。
そこで、TaskController
に @RequiredArgsConstructor
を付与し、手書きしていた TaskController
のコンストラクタを削除しましょう。
taskService
フィールドに final 修飾子がついているため、@RequiredArgsConstructor
アノテーションによって、TaskService
を引数に取るコンストラクタが生成されます。
@Controller
@RequiredArgsConstructor // このアノテーションを付ける
public class TaskController {
// final がポイント
private final TaskService taskService;
// @RequiredArgsConstructor によって、以下のコンストラクタが自動生成される
// public TaskController(TaskService taskService) {
// this.taskService = taskService;
// }
}
まとめ:大規模な Web アプリケーション開発にはコードを整理する戦略が必須
Dependency Injection (DI) は Spring Framework の中核的な機能です。 DI は、柔軟なコード設計とテスト容易性を実現するために欠かせません。 コンストラクタインジェクションを使用し、依存性を明示的に管理することで、よりメンテナンス性の高いアプリケーションを構築できます。