この記事はレコチョク Advent Calendar 2022の11日目の記事となります。
はじめまして、レコチョク新卒入社2年目の早坂と申します。
現在バックエンドシステムの構築に携わっており、Javaのフレームワークである、Spring Frameworkを用いた開発を行っています。
その中で、DIを勉強する機会があったので勉強した内容を記事にしました。
本記事の位置づけ
- オブジェクト指向プログラミングを経験したはあるが、DIがよくわかっていない
- Spring Frameworkを使って機能を実装しているけどDIをなんとなく使っている
といった方がDIの仕組みや実装を理解するための一助となるような記事になればと思います。
なお、上記1.の方は記事前半("なぜDIをするのか"まで)、2.の方は記事後半("Spring FrameworkにおけるDIの実現方法"から)を中心に読んでいただくと良いかと思います。
DI(Dependency Injection)とは
要するに
オブジェクト間の依存関係を弱めることを目的としたオブジェクト指向プログラミングにおけるデザイン・パターンの一種です。
メリット
- モジュール間の結合が疎となることでモックやスタブが導入しやすくなり、単体テストが容易になる
- DIを用いない場合に比べてシステムのエンハンス時に影響範囲が小さくなる
デメリット
- DIを実現することでクラス数が多くなったりフレームワーク導入を考慮する必要があるため、規模が小さいシステムには向かない
- DIコンテナ(後述)のような依存関係を管理するソフトウェアへの負荷が高くなることで、プログラムの実行スピードが低下する場合がある
なぜDIをするのか
記事冒頭でDIを下記のように表現しました。
オブジェクト間の依存関係を弱めることを目的としたオブジェクト指向プログラミングにおけるデザイン・パターンの一種です。
この章では、なぜDIが必要なのかを説明するために依存関係とは何か、それを弱めるとは何かについて記載します。
依存関係とは
例として、名前と楽曲情報を持つアーティストの情報をDBに登録する場合を考えてみます。
アーティストの情報を登録する前に楽曲のエンコード処理とアーティストが登録済かチェックする処理があるとします。
public interface ArtistService {
// DBへの登録処理を内包したビジネスロジック
void register(Artist artist, Music music);
}
public interface MusicEncoder {
// 楽曲のエンコード処理
String encode(Music music);
}
public interface ArtistRepository {
// DBへの登録処理
User save(Artist artist);
// アーティストが登録済かチェックする処理
boolean exists(Artist artist);
}
public class ArtistServiceImpl implements ArtistService {
private final ArtistRepository artistRepository;
private final MusicEncoder musicEncoder;
public ArtistServiceImpl(javax.sql.Datasource datasource) {
this.artistRepository = new JdbcArtistRepository(datasource);
this.musicEncoder = new Mp3Encoder();
}
public void register(Artist artist, Music music) throws ArtistAlreadyRegisteredExeption {
// 既登録のアーティストがいた場合は例外をスローする
if (artistRepository.exists(artist)) {
throw new ArtistAlreadyRegisteredExeption();
}
// 楽曲のエンコードとアーティスト情報の登録
artist.setMusic(musicEncoder.encode(music));
this.artistRepository.save(artist);
}
}
ArtistServiceImpl
のコンストラクタでは、JdbcArtistRepository
とMp3Encoder
をインスタンス化してArtistServiceImpl
のフィールドに代入しています。
このとき、ArtistServiceImpl
はJdbcArtistRepository
とMp3Encoder
のオブジェクトがないとインスタンス化ができないため、ArtistServiceImpl
はJdbcArtistRepository
とMp3Encoder
に依存しているといえます。
言い換えると、あるクラスAを利用するために渡す引数やプロパティがあるとき、クラスAはその引数やプロパティに依存していると言えます。
クラス間の依存関係があると、例の場合下記のようなデメリットが発生します。
-
JdbcArtistRepository
やMp3Encoder
が実装中の場合、ArtistServiceImpl
の実装・テストができない -
Mp3Encoder
を別のクラスに適宜切り替えたいという要件が出た場合、ArtistServiceImpl
に切り替え用のロジックを加える必要がある
依存関係があると発生してしまうこのような問題を解決するため、依存関係を弱めるという考え方になります。
依存関係を弱めることとDIをする理由
前述のようなデメリットが発生する原因として、上記のArtistServiceImpl
では
- 依存しているオブジェクトを直接指定している
- オブジェクトのインスタンス化や代入を直接コードに記述している
ことが考えられます。
1.は、コンストラクタでJdbcArtistRepository
とMp3Encoder
のインスタンスがフィールドに代入されており、依存関係にあるクラスが明記されていること指しています。
また、2.では、コンストラクタでJdbcArtistRepository
とMp3Encoder
のインスタンス化と代入を行っていることを指しています。
この2点を解決できれば、依存関係を弱めることに繋がります。
結論、DIをする理由は1.である、依存しているオブジェクトを直接指定している問題を解決することにあります。
これを回避するには、インスタンス生成をコンストラクタの外部に出すことが考えられますが、1つの例として、インターフェースを渡す方法が考えられます。
public ArtistServiceImpl(ArtistRepository artistRepository, MusicEncoder musicEncoder) {
this.artistRepository = artistRepository;
this.musicEncoder = musicEncoder;
}
インターフェースを介すことで、コンストラクタ内部にあったインスタンス化処理がなくなりました。
クラス図に示すと、下記のようになります。
クラス図で見てみると依存関係が可視化され、インターフェースが間に入る構図になっていることにより明確な依存関係の指定がなくなっていることがわかるかと思います。
しかし、結局ArtistServiceImpl
をインスタンス化する際には、下記のように直接ArtistRepository
とMusicEncoder
を渡さないといけません。結局、インターフェイスを渡すように修正したところで、下記のように何を呼び出すのかを直接定義しないといけないことになります。ここで2.が出てきます。
ArtistRepository jdbcArtistRepository = new JdbcArtistRepository(datasource);
MusicEncoder musicEncoder = new Mp3Encoder();
ArtistService artistService = new ArtistServiceImpl(jdbcArtistRepository, mp3Encoder);
この問題は、後述するDIコンテナを用いることで解決することができます。
Spring FrameworkにおけるDIの実現方法
Spring Frameworkでは、DIコンテナと呼ばれるソフトウェアを使用してDIを行うことができます。
DIコンテナについて
DIコンテナは、端的にいうとインスタンスの生成と依存関係を管理してくれるソフトウェアです。
DIコンテナを使用することで、インスタンスの生成と依存関係の解決をプログラムによって自動で行うことができ、前述した2.を解決することができます。
DIコンテナを使用した場合の依存関係解決イメージ
DIコンテナを利用するには、
- Beanの登録
- インジェクション
を行う必要があります。
Spring Frameworkでは、DIコンテナに登録して管理させるクラスをBean、Beanを登録するための定義をBean定義と言います。
また、本記事では、あるクラスのインスタンスを生成する際、依存関係にあるクラスのインスタンスを渡すことをインジェクションと定義します。
前述したコードのクラス図を用いてDIコンテナのイメージを説明します。
上図では、1でArtistRepository
とMusicEncoder
をDIコンテナに登録し、2でArtistServiceImpl
にインジェクションしています。
もしMp3Encoder
ではなく、WavEncoder
というクラスがあったときにWavEncoder
を利用したい場合は、下図のようにクラス登録を行った上でインジェクションするクラスを変更することができます。(図中赤線)
DIコンテナの概要やイメージを説明したところで、次にDIコンテナを利用してどのようにDIを実現するのかを説明していきます。
DIコンテナによるDI
前章で説明したクラス登録、インジェクションの順に説明していきます。
Beanの登録
DIコンテナへBeanを登録するには、Bean定義とコンポーネントスキャンを行う必要があります。
ちなみにここでのコンポーネントとはDIコンテナで生成したインスタンスを指しています。また、コンポーネントスキャンとはBean定義されたクラスを読み取ることです。これによりDIコンテナへ登録がなされます。
なお、Bean定義の方法によってコンポーネントスキャンの方法も異なるので、Bean定義の方法ごとに3つ記載します。
1.アノテーションベース
Bean定義
@Component
アノテーションをクラスに付与する形でBean定義を行う方法です。
また、@Component
はコンポーネントスキャン時にDIコンテナに登録するBeanを認識するためのマーカーとして機能します。
@Component
public interface ArtistService {
void register(Artist artist, Music music);
}
@Component
の他にも、下記のアノテーションはBeanを認識するためのマーカーとして機能します。
@Controller
@RestController
@Service
@Repository
コンポーネントスキャン
アノテーションベースのコンポーネントスキャンには、2通りの方法があります。
@ComponentScan
を利用した方法
@Configuration
が指定されているクラスに@ComponentScan
を付与します。
指定したパッケージ配下のクラスがスキャン対象となります。
@Configuration
@ComponentScan("[読み取りたいパッケージのパス]")
public class AppConfig {
}
なお、@ComponentScan
にパスを指定しない場合、@Configuration
を付与したクラスがあるパッケージ配下のクラスがスキャン対象となります。
例えば、下記のようなパッケージ構成があるとします。
com
└── springapp
├── AppConfig.java
├── AppInitializer.java
├── application
│ └── SampleApplication.java
│
└── domain
├── model
AppConfig.java
でコンポーネントスキャンを行う場合、下記のようなパッケージ構成だとAppInitializer.java
とapplication
、domain
パッケージ下にあるクラスがすべてスキャン対象となります。
@SpringBootApplication
を利用した方法
@SpringBootApplication
にscanBasePackages = "[読み取りたいパッケージのパス]"
を追加します。
記載したパッケージのパス配下にあるBean定義が読み取られ、DIコンテナに読み取ったクラスのインスタンスが登録されます。
@SpringBootApplication(scanBasePackages = "[読み取りたいパッケージのパス]")
public class SampleApplication {
}
@SpringBootApplication
は、@ComponentScan
を内包しているアノテーションであるため、この方法でもコンポーネントスキャンができます。(参考:Spring Boot API - Annotation Interface SpringBootApplication)
なお、scanBasePackages
を指定しない場合は、@ComponentScan
と同じパッケージの範囲がスキャン対象となります。
メリット
アノテーションを付与したクラスをコンポーネントスキャンで読み取るだけなので、簡易的にDIコンテナへの登録ができる
デメリット
任意の値を設定した上でDIコンテナに登録したい場合は向かない
2.Javaベース
Bean定義
アノテーションのようなマーカーを付与するだけのBean定義ではなく、JavaコードでBean定義を行う方法です。
@Configuration
アノテーションを付与したクラスに、@Bean
アノテーションを付与したメソッドを定義します。
@Bean
アノテーションが付与されたメソッドには戻り値としてクラスのインスタンスを設定します。
設定した戻り値がBeanとしてDIコンテナに登録されます。
@Configuration
public class AppConfig {
@Bean
ArtistRepository artistRepository() {
return new ArtistRepositoryImpl();
}
@Bean
MusicEncoder musicEncoder() {
return new Mp3Encoder();
}
@Bean
ArtistService artistService() {
return new ArtistServiceImpl(artistRepository(), musicEncoder());
}
}
コンポーネントスキャン
アノテーションベースの場合で記載した@SpringBootApplication
ですが、@ComponentScan
だけでなく、@EnableAutoConfiguration
というアノテーションも内包されています。
この@EnableAutoConfiguration
は、@Configuration
が付与されているクラスを探します。
なので、@SpringBootApplication
があるだけでコンポーネントスキャンが行われます。
@SpringBootApplication
public class SampleApplication {
}
メリット
Javaコードにより任意の設定をした上でDIコンテナへ登録ができる
デメリット
Bean定義をDIコンテナに登録したい分記述する必要があるため、コストがかかる
3.XMLベース
Bean定義
XMLにBeanの内容を記述することでBean登録を行うことができます。
下記のように、XMLにクラスやそのコンストラクタ定義、依存関係をクラスに直接記載せずに記述することができます。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xml">
<bean id="artistRepository" class="com.example.demo.ArtistRepositoryImpl"/>
<bean id="musicEncoder" class="com.example.demo.Mp3Encoder"/>
</beans>
<beans>
要素で複数のBean定義、<bean>
要素のid
属性で指定した値がBeanの名前、class
属性で指定したクラスがBeanのインスタンスとして定義されます。
XMLベースでのBean定義も全クラス分記載しないといけないですが、アノテーションベースと組み合わせたBean定義を行うことで設定を省略させることができます。
XMLでの記載上はvalue属性にString型のようになりますが、DIを通じて自動で任意の型に変換されます。
コンポーネントスキャン
下記のように記述すると、XMLによりコンポーネントスキャンを行うことができます。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xml
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xml">
<context:component-scan base-package="[読み取りたいパッケージのパス]"/>
</beans>
メリット
- Javaコードにより任意の設定をした上でDIコンテナへ登録ができる
- Bean定義をコードではなく外部ファイルで管理できる
デメリット
- Bean定義にコストがかかる
- DIコンテナに登録したい分記述する必要がある
- 記述量が多くなるため管理方法の検討が必要
Bean定義方法の使い分け(参考)
Bean定義方法は1つのみを選択しなければいけないというわけではなく、それぞれの効果を考慮して開発者自身が使い分けることができます。
XMLベースについては私自身使用したことがないので、アノテーションベースとJavaベースの使い分けについて記載します。
アノテーションベースはクラスにアノテーションを付与するためライブラリの編集を行う必要があります。
そのため、サードパーティライブラリのクラスをDIコンテナで管理させる場合は、ライブラリの編集はせずにJavaベースによってBean登録を行ったりします。
ちなみに私が携わっているシステムでは、アノテーションベースとJavaベースのBean定義を併用してDIを行っています。
インジェクション
前章までで、DIコンテナで依存関係の管理ができるようになりました。
後は依存関係をインジェクションするための定義をすれば、DIコンテナに登録されたBeanを参照してプログラムが自動的にDIを行います。
Spring Frameworkで提供されている3つのインジェクション方法を記載します。
1.コンストラクタインジェクション(推奨)
DI先クラスのコンストラクタを利用する方法です。
DIコンテナに登録されているオブジェクトを引数に渡すことで依存関係にあるBeanをインジェクションします。
Spring公式ドキュメントによると、コンストラクタインジェクションは推奨されているインジェクション方法です。
Spring Framework コアテクノロジー "コンストラクターベースまたは setter ベースの DI ?" より
※公式ドキュメントは英語のみのため、非公式ですが日本語訳のページを引用します
Spring チームは通常、アプリケーションコンポーネントを不変オブジェクトとして実装し、必要な依存関係が null でないことを保証できるため、コンストラクター注入を推奨しています。
アノテーションベースでBean定義した場合
@Component
public class ArtistServiceImpl {
private final ArtistRepository artistRepository;
private final MusicEncoder musicEncoder;
public ArtistServiceImpl(ArtistRepository artistRepository, MusicEncoder musicEncoder) {
this.artistRepository = artistRepository;
this.musicEncoder = musicEncoder;
}
}
ちなみに上記のサンプルコードですが、Spring4.3以降の定義になります。
Spring4.3より前のバージョンにおいては、下記のようにコンストラクタの定義の上に@Autowired
を記述する必要があります。
@Autowired
public ArtistServiceImpl(ArtistRepository artistRepository, MusicEncoder musicEncoder) {
this.artistRepository = artistRepository;
this.musicEncoder = musicEncoder;
}
JavaベースでBean定義をした場合
@Bean
ArtistServiceImpl ArtistServiceImpl(ArtistRepository artistRepository, MusicEncoder musicEncoder) {
return new ArtistServiceImpl(artistRepository, musicEncoder);
}
XMLベースでBean定義をした場合
<bean id="artistServiceImpl" class="com.example.demo.ArtistServiceImpl">
<constructor-arg name="artistRepository" ref="artistRepository"/>
<constructor-arg name="musicEncoder" ref="musicEncoder" />
</bean>
メリット
- フィールドをfinalとして宣言でき不変なオブジェクトを生成できるので、不本意に値が書き換えられることがない
- 循環参照を起動時にエラーとして検知することができる
デメリット
- コードの記述量が上記のインジェクション方法と比較して多くなる
ただし、Lombokのアノテーションを使用することで、記述量を減らすことができます。
2.セッターインジェクション
DI先クラスのセッターを利用する方法です。
コンストラクタインジェクションと同じく、引数にDIコンテナに登録されているオブジェクトを渡すことで依存関係にあるBeanをインジェクションします。
アノテーションベースを用いた場合
@Autowired
をセッターメソッドに記述することで、引数に渡しているBeanがインジェクションされます。
public class ArtistServiceImpl {
private ArtistRepository artistRepository;
private MusicEncoder musicEncoder;
@Autowired
public setArtistRepository(ArtistRepository artistRepository) {
this.artistRepository = new artistRepository;
}
@Autowired
public setMusicEncoder(MusicEncoder musicEncoder) {
this.musicEncoder = new musicEncoder;
}
}
Javaベースを用いた場合
@Bean
ArtistServiceImpl ArtistServiceImpl(ArtistRepository artistRepository, MusicEncoder musicEncoder) {
ArtistServiceImpl artistServiceImpl = new ArtistServiceImpl();
artistServiceImpl.setArtistRepository(artistRepository);
artistServiceImpl.setMusicEncoder(musicEncoder);
return artistServiceImpl;
}
Javaベースを用いた場合、直接インスタンスの生成を記述しているため、Bean定義によって依存関係を弱められている感覚は薄いかもしれないです。
XMLベースを用いた場合
<bean id="artistServiceImpl" class="com.example.demo.ArtistServiceImpl">
<property name="artistRepository" ref="artistRepository" />
<property name="musicEncoder" ref="musicEncoder" />
</bean>
メリット
- デフォルト値を設定してインスタンスを生成できる
デメリット
- 依存するクラスが多いとその数分のセッターを作成する必要がある
- フィールドをfinalにすることができないためDIしたオブジェクトの内容が書き換わってしまう可能性がある
3.フィールドインジェクション(非推奨)
記載している通り、現在(2022年11月時点)は非推奨とされているインジェクション方法です。(Spring公式ドキュメントにはそもそも"フィールドインジェクション"という記載がありません)
非推奨でも内容は確認はしたいという方は下記をお読みください。
フィールドインジェクションは、インジェクションしたいフィールド定義の上に@Autowired
を付与することで、依存関係のあるBeanをインジェクションすることができます。
なお、DI先のクラスも事前にBean登録してDIコンテナの管理下においておく必要があるため、DI先のクラスにはBean定義でも使用した@Component
を付与する必要があります。
@Component
public class ComponentObject {
@Autowired
private Material material;
}
メリット
- コンストラクタやセッターを利用する必要がないため、簡潔に記述することができる
デメリット
- コンストラクタやセッターを利用しないためDIコンテナありきのインジェクション方法となる。
そのため、実行環境がSpring Frameworkに依存してしまうのでスタンドアロンなライブラリとして公開するには適切 - フィールドをfinalにすることができないためDIしたオブジェクトの内容が書き換わってしまう可能性がある
コンストラクタインジェクションとフィールドインジェクションの使い分け
ここまで読んだ方は、インジェクション方法をどう使い分けるのか気になったかと思います。
Spring Framework コアテクノロジー "コンストラクターベースまたは setter ベースの DI ?" より
※公式ドキュメントは英語のみのため、非公式ですが日本語訳のページを引用します
コンストラクターベースの DI と setter ベースの DI を混在させることができるため、必須の依存関係にはコンストラクターを使用し、オプションの依存関係には setter メソッドまたは構成メソッドを使用することをお勧めします。
Setter インジェクションは、主に、クラス内で適切なデフォルト値を割り当てることができるオプションの依存関係にのみ使用する必要があります。
必須の依存関係というのは、null
を許容せず、何かしらの値を初期値に持つ必要がある依存関係です。
また、オプションの依存関係とは、null
を許容する依存関係を指しています。
まとめると、下記のような認識になります。
nullを許容しない依存オブジェクト→コンストラクタインジェクション
nullを許容するかつ場合によって任意の値を設定したい依存オブジェクト→フィールドインジェクション
こういったルールが明確にあると、コードを読む側になったときに依存関係がわかりやすいですし、かつ安全な設計になるかと思います。
最後に
DIの概要とSpring FrameworkにおけるDIの方法について記載しました。
本記事ではSpring FrameworkのDIについて記載しましたが、Javaを用いた場合に関わらず、オブジェクト指向のプログラミングにおいては欠かせない考え方なので、今後も実装経験を積みながら理解を進められればと思います。
最後までお読みいただき、ありがとうございました。
明日のレコチョク Advent Calendarは12日目【Android 】Glanceを使ったアプリウィジェット
です。お楽しみに!
参考
- デザインパターンの基本
- Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発(書籍)
- Spring Framework Core Technologies(公式ドキュメント:英語のみ)
- Spring Framework コアテクノロジー リファレンス(非公式ドキュメント:公式の日本語訳)
- 循環参照
- Lombokアノテーション
この記事はレコチョクのエンジニアブログの記事を転載したものとなります。