はじめに
自社開発のエンジニアをやっているものです。
普段バックエンドではspringbootを使っているのですが、初めはDIの仕組みやメリットに対する理解が定着しなく、DIに関する実装は通例的な感覚で書いていました。
それから1年間経験した結果、ある程度理解できたと思うのでアウトプットしてより理解度を高めていきたいと考えました。
DIとは
DIはあるクラスからみて依存するであろうクラスの切り替えを容易にする機能を持っていると考えています。
上記で表現した「依存」について説明します。
プログラムにおける依存とは
以下のクラスがあるとします。(細かい設計云々の不備は許容してください><)
- UserService(ユーザの情報に関するビジネスロジックを実施してくれる)
public class UserService{
UserRepository userRepository = new UserRepository();
// UserRepositoryUnitTest userRepository = new UserRepositoryUnitTest(); *1
List<UserResource> fetchUser(UserId userId){
// ユーザ情報を取得、整形して返すメソッドや整形、登録するメソッドがある
}
}
- UserRepository(ユーザに関する情報のCRUD処理を実施してくれる)
public class UserRepository{
// DBにアクセスしてユーザ情報のテーブルを操作するメソッドがある
}
- UserRepositoryUnitTest(呼ぶ側の単体テストを実施するためのクラス)
public class UserRepositoryUnitTest{
// UserRepositoryと同じメソッドを持っているがそれぞれ単に固定値を返す処理になっている
}
実運用ではもちろんUserRepositoryを使用したいが、UserServiceの単体テストを行いたいときはUserRepositoryUnitTestを使用したい。そのような切り替えをする際にはUserServiceの*1の部分のコメントを適用してUserRepositoryの生成をコメントアウトにする必要があると考えられます。
この場合はUserServiceはUserRepositoryに対して「依存している」と言えます。
クラスの生成処理を実装した場合のデメリット
UserRepositoryを呼ぶクラスがuserServiceのみであると想像しにくいかもしれないのですが、仮に呼ぶクラスが50個とかになると切り替えが非常に面倒になることが考えられます。
上記は単体テスト用のクラスで例えましたが、DBがmysqlに変わるためUserRepositoryMySqlを作成してリファクタリングをする必要があった場合に全ての呼ぶクラス内の生成処理を変えないといけないことも負担&リファクタ漏れ等のリスクがあり保守性に欠けると考えられます。
依存するクラスに対するDIのアプローチ
springのDI機能を活用すると、状況に応じて適切な具象クラスを割り当ててくれます。
その結果依存するであろうクラスの切り替えが容易に実施することができます。
DIを活用するための詳しい手順は割愛しますが、ざっくり理解のための例を示します。
DI機能の実装例
先ほどの例を少し変更しました。
- UserService(ユーザの情報に関するビジネスロジックを実施してくれる)
@Service
public class UserService{
// 先ほどと違うところ始点
private UserRepository userRepository
@Autowired // *1
public UserService(UserRepository userRepository){
this.userRepository = userRepository;
}
// 先ほどと違うところ終点
List<UserResource> fetchUser(UserId userId){
// ユーザ情報を取得、整形して返すメソッドや整形、登録するメソッドがある
}
}
- UserRepository(ユーザ情報のリポジトリのインターフェース)
public interface UserRepository{
// DBにアクセスしてユーザ情報のテーブルを操作するメソッドがある
}
- UserRepository(実運用で使いたい)
@Repository
@Profile("Production")
public class UserRepositoryImpl implements UserRepository{
// DBにアクセスしてユーザ情報のテーブルを操作するメソッドがある
}
- UserRepositoryUnitTest(呼ぶクラスの単体テストで使いたい)
@Repository
@Profile("Test")
public class UserRepositoryUnitTest implements UserRepository{
// UserRepositoryと同じメソッドを持っているがそれぞれ単に固定値を返す処理になっている
}
ざっくり解説
以下の手順で修正
- UserRepositoryをインターフェースで定義をして実運用、テスト用の具象クラスはインターフェースを継承して定義
- 呼ぶ側(UserService)ではUserRepositoryをフィールド定義し、コンストラクタで引数のUserRepositoryをフィールド変数に格納する
- コンストラクタに@Autowiredを付与する *1
- 各具象クラスに@Profile("任意の文字列")をつける
このようにすることで実行環境によってUserRepositoryに適切なクラスのインスタンスを注入してくれるみたいです。アノテーションですが、@AutowiredはDIが必要な箇所に付与することでDIを実施する必要があると示すことができます。@Profileは文字列を指定することで起動コマンドで指定したプロファイル名に従い注入するクラスを制御することができます。
まとめ
今回はDIが必要な背景の理解を目的としているのでbean定義等の説明は割愛しました。
抽象的すぎて返ってわかりにくいかもしれないのですが、理解の手助けになれれば幸いです。
springにおけるDIの動きの詳細は別記事にてまとめようと思います。