普段、SpringBootで開発している中で改めてDIの概念について整理するためにまとめてみました。
SpringBootを利用していると@Autowiredや@Component等のアノテーションをつければ、サクッと依存するファイルが注入されて、開発者があまりインスタンスの生成などを意識しなくても開発できると思います。
そもそもDIとは何だっけというところを備忘録的にまとめました。
参考図書
DIとは
Dependency Injectionの略。
生成されるオブジェクトが必要な依存しているオブジェクトを自ら生成するのではなく、外部から注入すること
定義:
様々なソフトウェア設計の原理原則やパターンを集めて、コードが疎結合になるような開発を行えるようにするもの
簡単なコードだと下記のようにコンストラクタ経由でオブジェクトを渡しているコードも依存性の注入している一例です。
@Compose
public class OrderRepositoryImpl OrderRepository{
private final DbUtil dbUtil;
@Autowired
public OrderRepositoryImpl(@Nonnull DbUtil dbUtil){
this.dbUtil = dbUtil;
}
}
DIの目的
- コードの結合度下げることができる(オブジェクト同士の疎結合した結果)
- 柔軟性を高める
- 再利用性を高める
DIの特性
オブジェクトの合成
依存のオブジェクトを生成して、必要とするクラスに注入してオブジェクトを生成すること
クラス自体が依存を生成せずに外部から提供された依存関係を受け取り、制御を放棄することは対象のクラスがどのクラス実装クラスを利用するを選択しないということ
介入
依存するオブジェクトを対象のクラスに注入される前に追加の処理を加えたり、別のふるまいに変えたりすることができる。そのオブジェクトに対して変更を加える必要がない。
オブジェクトの生存期間
依存のオブジェクトがいつ生成されて、いつ破壊されるかの制御も利用するオブジェクトが知らなくてもよくなる。
アプリケーションで一度だけなのか、リクエストされる度なのか、クラスは自身は知らない。
DIするにはどんな方法があるか
- コンストラクタ経由での注入(Constructor Injection)
- 対象のクラスが必要とするすべての依存をそのクラスのコンストラクタに引数として定義することでどのような依存を必要とするかを静的に定義する
- 注:コンストラクタ経由の過度な注入
コンストラクタによる注入の数が増えていくことで、クラスの責務が肥大化して、単一責任の原則を満たしていない可能性が出てくる
public class HomeContoller{
private final IProductService service;
public HomeContoller(IProductService service){
if(service == null){
throw new ArguamentNullExeption("");
}
this.service = service;
}
- メソッド経由での注入(Method Injection)
- メソッドを呼び出す際に必要となる依存をその引数から渡すことで受け渡した依存を使えるようにする
- 注:一時的結合(コードの嫌な臭い)につながりうる
クラスの中にある異なるフィールドやメソッドが暗黙的に結びつく。
public class Component{
private ISomeInterface dependency;
public void init(ISomeInterface dependency){
this.dependency = dependency;
}
// ↓ doSomethingを呼ぶ前にinitを呼ぶ必要がある
public void doSomething(){
if(this.dependency == null){
throw new InvalidOperationException("Call InitFirst");
}
this.dependency.doStuff();
}
}
- プロパティ経由での注入(Method Injection):Setter経由での注入
- プロパティを公開し、Setter経由で置き換えられるようにすることでデフォルトとは異なる振る舞いをする依存を注入する方法
// 顧客エンティティ public class Customer{ public Guid id; public String name; public void setId(String name){ this.name = name; } }
- プロパティを公開し、Setter経由で置き換えられるようにすることでデフォルトとは異なる振る舞いをする依存を注入する方法
- DIコンテナ
-
オブジェクト合成・介入・生存管理等の依存注入
など依存注入に関するタスクを自動化してくれるライブラリを利用して依存性を注入する方法 - DIコンテナ自体はDBアクセス、ログ出力等どのようなシステムにも利用できる汎用的なソフトウェアと同様のもの
- オブジェクトの自動解決
- 要求された型に対して、事前に登録された情報(型の情報、対象に関連するクラスから抽出した型の情報)をもとに関連するオブジェクトを取得してオブジェクトグラフを構築する、
DIコンテナには抽象と実装クラスが事前に登録されていないといけない
- 要求された型に対して、事前に登録された情報(型の情報、対象に関連するクラスから抽出した型の情報)をもとに関連するオブジェクトを取得してオブジェクトグラフを構築する、
-
DIコンテナに登録する方法
- 手動登録:
- 構成ファイル:Json、Xmlなどを利用して構成ファイルに紐づけを定義する
- メリット:
- 登録情報を変更しても再コンパイルする必要がない
- デメリット:
- コンパイル時の確認ができない
- 冗長で壊れにくい
<!-- 構成ファイル xml --> <bean id="myBean" class="com.example.MyBean"> <property name="dependency" ref="myDependency"> </bean>
- メリット:
- コードによる登録:@Configuration
- コード上で明示的に定義する
- メリット:
- コンパイル時に確認される
- 細かな制御が可能
- デメリット:
- 登録情報を変更すると再コンパイルしないといけない
@Configuration public class RestTemplateConfig{ @Bean RestTemplate restTemplate(){ return new RestTemplateBuilder() .additionalInterceptors(new RestTemplateLoggingInterceptor()) .builder(); } }
- 構成ファイル:Json、Xmlなどを利用して構成ファイルに紐づけを定義する
- 自動登録:
- コンポーネントをスキャンして規約に沿ったクラスを見つけ出して抽出して実装クラスの紐づけを行う
- アノテーションを利用した登録
- @SpringBootApplicationや@ComponentScanを利用して対象のディレクトリをスキャンして、@Service、@Component、@Controllerなどのアノテーションが付与されたクラスを自動的に検出して、SpringのDIコンテナに登録
- メリット
- 登録情報を変更しても再コンパイルする必要がない
- 求められる労力が少ない
- 規約に従うことでコードベースの一貫性のあるものとなる
- デメリット
- コンパイル時の確認ができない
- 細かい制御ができない
- 慣れるまで登録内容に自信を持てない
まとめ
DIの書籍を読んでまとめてみることで、フレームワークの中でどのように依存関係が注入されているのか理解が進みました