今や様々なアプリケーションフレームワークで用いられているDI(Dependency Injection)と言う概念。 意外と意識せず使っている人が多いので、まとめてみる。
# DIとは
DI(依存性の注入)とは、コンポーネントが中で使っている他のコンポーネントを、外部から受け取るようにする仕組み。
# 具体的な実装イメージ
例えば動画を取得して再生できるVideoPlayerクラスで表すと…
# DIしない例
VideoPlayerは、動画を取得するためにVideoRepositoryを利用(依存)している。 そのため、VideoRepositoryインスタンスを自分の中で生成している。
class VideoPlayer{
// 自分でVideoRepositoryインスタンスを生成。
private VideoRepository repo = new VideoRepository();
public void play(String id){
Video video = repo.getVideo(id);
video.play();
}
}
class Application(){
public static void main(String[] args) {
VideoPlayer player = new VideoPlayer();
player.play("omosiroi_douga_id");
}
}
# DIする例
VideoRepositoryインスタンスを自前で用意せず、コンストラクタの引数で外部から受け取る。
class VideoPlayer{
private VideoRepository repo;
// 依存しているコンポーネントを外部から注入してもらう!
public VideoPlayer(VideoRepository repo){
this.repo = repo;
}
public void play(String id){
Video video = repo.getVideo(id);
video.play();
}
}
class Application(){
public static void main(String[] args) {
// VideoPlayerインスタンス生成時にVideoRepositoryを注入する!
VideoPlayer player =
new VideoPlayer(new VideoRepository());
player.play("omosiroi_douga_id");
}
}
# DIすると何がうれしい?
# 単体テストしやすい
依存するコンポーネントを外から注入できるので、モックオブジェクトを注入することができる。 そのため、テストする範囲を限定させることができる。
例えば先程のVideoPlayer#playの単体テストをしようとしたとき、 DIしない例ではVideoPlayer#playを動かすとVideoRepository#getVideoまで実行されてしまい、テストの際に考慮する範囲が広くなってしまう。
DIにして、VideoRepositoryの代わりにモックを注入すれば、VideoPlayer#playのテストのみを実施できる。
class TestVideoPlayer{
public void test(){
VideoPlayer player =
new VideoPlayer(new MockVideoRepository());
player.play("test");
}
}
class MockVideoRepository extends VideoRepository{
public Video getVideo(String id){
// DBに検索しにいかず、固定でVideoを返すモック
return new Video();
}
}
# 疎結合
外からコンポーネントを注入できることで、流用性を高められる。 例えば、
- youtubeから動画を取得するYoutubeVideoRepository
- ニコニコ動画から動画を取得するNiconicoVideoRepository
があるとする。
DIなしだと以下のようになり、両方に対応したプレーヤーが作れない。
class VideoPlayer{
// ニコニコ動画を再生するにはもう1種類のVideoPlayerが必要。。。
private VideoRepository repo = new YoutubeVideoRepository();
public void play(String id){
Video video = repo.getVideo(id);
video.play();
}
}
DIありなら、VideoPlayerの利用者がどちらのRepositoryを渡すかで、同じVideoPlayerを使い回せる。
class VideoPlayer{
private VideoRepository repo;
public VideoPlayer(VideoRepository repo){
this.repo = repo;
}
public void play(String id){
Video video = repo.getVideo(id);
video.play();
}
}
class Application(){
public static void main(String[] args) {
VideoPlayer youtubePlayer =
// youtubeから取得!
new VideoPlayer(new YoutubeVideoRepository());
youtubePlayer.play("youtube douga");
VideoPlayer niconicoPlayer =
// ニコニコから取得!
new VideoPlayer(new NiconicoVideoRepository());
niconicoPlayer.play("niconico douga");
}
}
# フレームワークのDI機能
フレームワークによっては、設定ファイルやアノテーションで定義するだけで自動的にDIを行える。
例えば、Springでは@Autowiredアノテーションを用いる。
@Component
class VideoPlayer{
@Autowired
VideoRepository repo;
public void play(String id){
Video video = repo.getVideo(id);
video.play();
}
}
仕組みとしては以下の図のようになっている。 [図] アプリケーション起動時に、@Component等のアノテーションがついている全てのクラスのインスタンスを生成して、DIコンテナという場所に格納する。 そして@Autowiredが書かれたクラスに対して、DIコンテナからインスタンスを取り出して注入するのだ。
# DIコンテナ
DIコンテナ機能ではアプリ内で一つのインスタンスを使い回すため、メモリの節約にもなる。
逆に注意しないといけない点として、DI登録するコンポーネントには絶対に値を保持しないこと。 インスタンスが使い回されるため、保持している値がどこで変更されるか分からなくなる。