公開日:6/6/2021 更新日:3/26/2022
本記事では依存関係逆転の原則と、それを解決するDIパターンのソースコードのサンプルを備忘録として残します。ソースコードは、【黒本著者が教える やさしくない!? Java】の Youtube動画の内容を書き起こしたモノとなっています。大変勉強になるので、同チャンネルのリフレクションの話のシリーズ(1~7話)を全て視聴することをお勧めします。
Dependency Injection (依存性注入) の略。依存性注入とはソフトウェアのうち、外部の環境などに依存する部分(データへのアクセスなど)を切り離し、 ソフトウェアの外部から提供できるようにするという考え方です。
上図を使って分かりやすく説明します。例えば、Cモジュールを参照するBモジュール、さらにBモジュールを参照するAモジュールが存在するとします。その際に、AモジュールがBモジュールを使用するには、BモジュールがCモジュールを使える状態になっていないといけません。つまり、Aモジュールから見てBモジュールはCモジュールに依存している状態です。BモジュールがBモジュールであるためにCモジュールが必要であることをオブジェクトアイデンティティといいます。そのオブジェクトアイデンティティを外部から確立する作業が Dependency Injection (依存性注入) というわけです。
ちなみにDIには以下の3種類が存在するようです。
次にDIコンテナとは、アプリケーションにDI (Dependency Injection: 依存性注入) 機能を提供するフレームワークです。DIコンテナはクライアントから依頼を受けると依存関係が解決した完成品のインスタンスを渡してくれます。また、DIコンテナを使用することで使用したいインターフェイスの切替を容易に行うことも出来ます。例えば、開発中はDBに実際に接続しないモックのインターフェイスに一時的に切り替えたりする時に使用したりします。
パーマネント領域 (ヒープ) に存在するクラスのメタ情報を扱うことで、クラスに定義されているコンストラクタやメソッド、フィールドの一覧を読み取ったり、そこからメソッドを呼び出したりフィールドの値を取り出したりということができます。つまり動的なインスタンスの使用が可能となります。Spring 等のフレームワークの裏側で使用されていて、日頃から見えないところでお世話になっている技術です。
ソースコードの全体像
サンプルプログラムは大きく分けて、4つのレイヤー (Presentation、BussinessLogic、DataAccess、Container) に分割されます。DIコンテナを用いることで、パッケージ間が片方依存となり相互依存を回避することに成功しています。DIコンテナを自前で実装するために、アノテーションとリフレクションを駆使してコーディングを行います。
コンテナはイメージ図の①~③の順に処理を実行します。
① ItemDaoImpl のインスタンスを生成
② ItemCatalogImplのインスタンスを生成
③ ItemDao に ItemDaoImpl への参照をセット (依存性の注入)
クラスの定義情報を直接受け取れるアノテーションクラス。
package annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Resource {
Class value();
}
いわゆる Bean のクラス。Itemの名前や値段等の情報を保持している。
package logic;
public class Item {
private int id;
private String name;
private int price;
public Item(int id, String name, int price) {
super();
this.id = id;
this.name = name;
this.price = price;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
}
Presentation レイヤーがBusinessLogicレイヤーにアクセスするためのインターフェイス。インターフェイスを介することでPresentation側はBusinessLogic側の仕様を知らなくて良くなる。
package logic;
import java.util.List;
public interface ItemCatalog {
public static ItemCatalog getInstance() {
return new ItemCatalogImpl();
}
public List<Item> getAll();
}
ItemCatalogを継承したImplクラス。自前で用意したアノテーションにより、ItemDaoImplのインスタンス生成して ItemDao 型の変数daoにセットしている。具体的なセット方法の仕組みは後述のContainerクラスで実装している。ItemDaoImplクラスのfindAllメソッドを実行して、List<Item> 型のリストを返却している。
package logic;
import java.util.List;
import annotation.Resource;
import dataaccess.ItemDaoImpl;
public class ItemCatalogImpl implements ItemCatalog {
@Resource(ItemDaoImpl.class )
private ItemDao dao;
public List<Item> getAll(){
return dao.findAll();
}
}
BusinessLogicレイヤーがDataAccessレイヤーにアクセスするためのインターフェイス。
package logic;
import java.util.List;
import dataaccess.ItemDaoImpl;
public interface ItemDao {
public static ItemDao getInstance() {
return new ItemDaoImpl();
}
List<Item> findAll();
}
ItemDao を継承したImplクラス。本来はDBなどにアクセスするクラスだが、今回は簡略なItemリストを返すだけ。
package dataaccess;
import java.util.ArrayList;
import java.util.List;
import logic.Item;
import logic.ItemDao;
public class ItemDaoImpl implements ItemDao {
@Override
public List<Item> findAll(){
List<Item> list = new ArrayList<Item>();
list.add(new Item(100,"orange",60));
list.add(new Item(101,"apple",100));
list.add(new Item(102,"banana",80));
return list;
}
}
今回の肝となるDIコンテナのクラス。冒頭で説明した、①~③の役目を担っている。引数で渡されたクラス情報からそのクラス(ItemCatalogImpl) のフィールドを順番に取り出し、Resource アノテーションが引っ付いているフィールドに対して、アノテーションで指定したクラスのインスタンス (ItemDaoImpl) をインスタンス (ItemCatalogImpl) のフィールド (ItemDao型の変数) にセットして返却している。リフレクションによりクラスのメタ情報を扱って処理を行っている。
package core;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import annotation.Resource;
public class Container {
public Object getInstance(Class clazz) throws Exception {
Object obj = clazz.getConstructor().newInstance();
Field[] fields = clazz.getDeclaredFields();
for (Field f :fields) {
Annotation[] annotations = f.getAnnotations();
for (Annotation an:annotations) {
if (an instanceof Resource) {
Resource resource = (Resource) an;
Class target = resource.value();
f.setAccessible(true);
f.set(obj,target.getConstructor().newInstance());
}
}
}
return obj;
}
}
実行対象のメインクラス。コンテナ経由で、依存性が注入 (必要なItemDaoImplのインスタンスがセットされている) された ItemCatalogImpl のインスタンスを取得する。最後に、取得したItemリストを表示して正常動作を確認する。
package presentation;
import java.util.List;
import core.Container;
import logic.Item;
import logic.ItemCatalog;
import logic.ItemCatalogImpl;
public class Main {
public static void main(String[] args) throws Exception{
Container con = new Container();
ItemCatalog catalog = (ItemCatalog) con.getInstance(ItemCatalogImpl.class);
List<Item> items = catalog.getAll();
for (Item item:items) {
System.out.println(item.getName());
}
}
}
実行結果
apple
cherry
banana