依存性の注入
依存性の注入(いぞんせいのちゅうにゅう、英: Dependency injection)とは、あるオブジェクトや関数が、依存する他のオブジェクトや関数を受け取るデザインパターンである。英語の頭文字からDIと略される。DIは制御の反転の一種で、オブジェクトの作成と利用について関心の分離を行い、疎結合なプログラムを実現することを目的としている。
dependencyを「依存性」と訳すのは本来の意味[1] から外れているため「依存オブジェクト注入」の用語を採用する文献も複数存在する[2][3]。
概要
[編集]DIを利用したプログラムを作成する場合、コンポーネント間の関係はインタフェースを用いて記述し、具体的なコンポーネントを指定しない。具体的にどのコンポーネントを利用するかは別のコンポーネントや外部ファイル等を利用することで、コンポーネント間の依存関係を薄くすることができる。
依存関係がプログラムから外部に取り除かれることで、以下のようなメリットが発生する[4]。
Dependency injectionという用語を作成したのはソフトウェア開発者のマーティン・ファウラーである。類似の概念としてそれ以前から制御の反転 (IoC) と呼ばれるアイデアが存在していたが、それを整理・範囲を限定することでDIが生み出された。現在では代表的なDIコンテナとして知られるSpring Frameworkも、誕生当初はDIではなくIoCという表現を用いていた。DIは2000年代前半のJavaによる開発において、極めて複雑な標準仕様となっていたJava EE(現・Jakarta EE)の特にEJBに対する批判を背景に広く用いられるようになった[4]。 その概念は後に標準仕様にも取り込まれ、2007年のJava EE 5では限定的な機能を備えたEJB 3.0が、2009年のJava EE 6ではより汎用的なDIコンテナとしての機能を備えたCDIが定義されている[5]。
DIの種類
[編集]プログラムに依存性を注入する方法としては、以下のような手法が存在する。
例
[編集]DIの例として、以下にJavaによるDIを用いない場合と手動でのDI、ならびにDIコンテナをイメージした自動でのDIのサンプルコードを示す。
初めに、一連のサンプルで用いる各コンポーネントのインタフェースを、株式売買を例題にして示す。
public interface IOnlineBrokerageService {
String[] getStockSymbols();
double getBidPrice(String stockSymbol);
double getAskPrice(String stockSymbol);
void putBuyOrder(String stockSymbol, int shares, double buyPrice);
void putSellOrder(String stockSymbol, int shares, double sellPrice);
}
public interface IStockAnalysisService {
double getEstimatedValue(String stockSymbol);
}
public interface IAutomatedStockTrader {
void executeTrades();
}
DIを用いない状態
[編集]以下はDIを用いない場合の実装例である。
public class VerySimpleStockTraderImpl implements IAutomatedStockTrader {
private IStockAnalysisService analysisService = new StockAnalysisServiceImpl();
private IOnlineBrokerageService brokerageService = new NewYorkStockExchangeBrokerageServiceImpl();
public void executeTrades() {
…. // omitted
}
}
public class MyApplication {
public static void main(String[] args) {
IAutomatedStockTrader stockTrader = new VerySimpleStockTraderImpl();
stockTrader.executeTrades();
}
}
VerySimpleStockTraderImpl
クラスでは、直接IStockAnalysisService
, IOnlineBrokerageService
インタフェースを実装したクラスのインスタンスを作成しており、これらの実装に深く依存してしまっている。
手動でのDI
[編集]上記のコードを、手動でDIを行うようにリファクタリングすると下記のようになる。
public class VerySimpleStockTraderImpl implements IAutomatedStockTrader {
private IStockAnalysisService analysisService;
private IOnlineBrokerageService brokerageService;
public VerySimpleStockTraderImpl(
IStockAnalysisService analysisService,
IOnlineBrokerageService brokerageService) {
this.analysisService = analysisService;
this.brokerageService = brokerageService;
}
public void executeTrades() {
…
}
}
public class MyApplication {
public static void main(String[] args) {
IStockAnalysisService analysisService = new StockAnalysisServiceImpl();
IOnlineBrokerageService brokerageService = new NewYorkStockExchangeBrokerageServiceImpl();
IAutomatedStockTrader stockTrader = new VerySimpleStockTraderImpl(
analysisService,
brokerageService);
stockTrader.executeTrades();
}
}
この例では、MyApplication.main()
が依存性の注入を行っており、VerySimpleStockTraderImpl
自体は特定の実装に依存しなくなっている。なお、この実装ではコンストラクタ注入の手法が用いられている。
自動的なDI
[編集]DIコンテナを用いることで、依存性の注入をコード上に直接記述せず、自動的に行うことが可能である。こうした手法を用いる場合、依存性は外部のXMLファイルやメタデータにて定義する。上記のコードを、XMLを用いるDIコンテナを使用するようリファクタリングした例が下記である。
<contract id="IAutomatedStockTrader">
<implementation>VerySimpleStockTraderImpl</implementation>
</contract>
<contract id="IStockAnalysisService" singleton="true">
<implementation>StockAnalysisServiceImpl</implementation>
</contract>
<contract id="IOnlineBrokerageService" singleton="true">
<implementation>NewYorkStockExchangeBrokerageServiceImpl</implementation>
</contract>
public class VerySimpleStockTraderImpl implements IAutomatedStockTrader {
private IStockAnalysisService analysisService;
private IOnlineBrokerageService brokerageService;
public VerySimpleStockTraderImpl(
IStockAnalysisService analysisService,
IOnlineBrokerageService brokerageService) {
this.analysisService = analysisService;
this.brokerageService = brokerageService;
}
public void executeTrades() {
… // omitted
}
}
public class MyApplication {
public static void main(String[] args) {
IAutomatedStockTrader stockTrader =
(IAutomatedStockTrader) DependencyManager.create(IAutomatedStockTrader.class);
stockTrader.executeTrades();
}
}
この例では、IAutomatedStockTrader
のどの実装を使用するかの判断はDIコンテナに委ねられている。インタフェースが要求されたDIコンテナは、設定ファイルに基づきその実装であるVerySimpleStockTraderImpl
クラスのインスタンスを返す。さらに、VerySimpleStockTraderImpl
のIStockAnalysisService
とIOnlineBrokerageService
の依存性に対して、同様にコンストラクタ注入を行う。
DIコンテナには数多くの種類があり、上で示した例はそのごく一部でしかない。実際にはDIコンテナごとに様々な手法が用いられている。
DIを用いた単体テスト
[編集]DIを用いることで、単体テストにおいて簡単に依存性をテスト用のクラス(モックオブジェクト等)に差し替えることができる。以下はDIを用いた、前述のVerySimpleStockTraderImpl
クラスのテストケースの例である。この例では、IOnlineBrokerageService
, IStockAnalysisService
インタフェースを実装したテスト用クラスを作成し、DIによりそれを注入することで、実際のクラスを用いることなく、単体テストを実現している。
public class VerySimpleStockBrokerTest {
// IOnlineBrokerageServiceを実装した単純なスタブ
public class StubBrokerageService implements IOnlineBrokerageService {
public String[] getStockSymbols() {
return new String[] {"ACME"};
}
public double getBidPrice(String stockSymbol) {
return 100.0; // (テストに十分な値)
}
public double getAskPrice(String stockSymbol) {
return 100.25;
}
public void putBuyOrder(String stockSymbol, int shares, double buyPrice) {
Assert.Fail("Should not buy ACME stock!");
}
public void putSellOrder(String stockSymbol, int shares, double sellPrice) {
// このテストでは使用しない
throw new NotImplementedException();
}
}
public class StubAnalysisService implements IStockAnalysisService {
public double getEstimatedValue(String stockSymbol) {
if (stockSymbol.equals("ACME"))
return 1.0;
return 100.0;
}
}
public void TestVerySimpleStockTraderImpl() {
// このテスト専用の依存性を指定するため、DIコンテナに直接登録している
DependencyManager.register(
IOnlineBrokerageService.class,
StubBrokerageService.class);
DependencyManager.register(
IStockAnalysisService.class,
StubAnalysisService.class);
IAutomatedStockTrader stockTrader =
(IAutomatedStockTrader) DependencyManager.create(IAutomatedStockTrader.class);
stockTrader.executeTrades();
}
}
実装がDBやネットワークにアクセスする場合、また古いEJBのような重たいコンポーネントの場合、そのままでは単体テストを行うことは難しい。しかし、上記のようにDIを用いて依存関係のみをテスト用のものに差し替えることで、本来のテスト対象のプログラムには手を加えることなく、簡単に単体テストを行うことができる。[4]
HTML
[編集]マークアップ言語であるHTML (HyperText Markup Language) でも依存性の注入がおこなわれる。
WebComponents(カスタム要素 + Template要素+ ShadowDOM)の登場により、巨大なHTMLファイルを小さなHTML要素コンポーネントの集合として記述することが可能になった。しかし大きなコンポーネント Big が小さなコンポーネント Small を包む形でコーディングすると、Big が Small に依存してしまう。そこでslot要素を用いた依存性の注入がおこなわれる。slot要素は弱いinterfaceとして働き、slot要素を用いて定義されたカスタム要素を利用する際に依存性をタグで囲むことで注入できる。
下記の例では大きなコンポーネント<my-element-with-slot>が2つの受け入れ可能slotを持っている。利用時にslotを指定したspan要素を挿入することで、<my-element-with-slot>はspan要素に直接依存せずにspan要素を利用できる。プログラミング言語のような明示的interfaceがない(interfaceによる型指定slot要素がない)ために型支援を受けた安全な依存性の注入は現時点ではおこなえないが、適切に設計することで依存性を切り分けることは可能である。
<!--when define-->
<script>
class myElementWithSlot extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
<h2>My Element</h2>
<h3>inserted #1: <slot name="slot1">no contents</slot></h3>
<h4>inserted #2: <slot name="slot2">no contents</slot></h4>
`;
}
}
customElements.define("my-element-with-slot", myElementWithSlot);
</script>
<!--when use-->
<my-element-with-slot>
<span slot="slot1">dependency-one</span>
<span slot="slot2">dependency-two</span>
</my-element-with-slot>
DIコンテナ
[編集]DIの機能を提供するフレームワークはDIコンテナと呼ばれる[4]。 主なDIコンテナとしては、下記のようなものが存在する。
Java
- Jakarta EE (EJB, CDI)
- Spring Framework
- Seasar2
- Google Guice
.NET
- Entity Framework
- Spring.NET
- Microsoft.Extensions.DependencyInjection[6]
PHP
注釈・出典
[編集]- ^ “Dependency Definition & Meaning - Merriam-Webster”. Merriam-Webster. 2022年9月3日閲覧。 “: something that is dependent on something else especially : a territorial unit under the jurisdiction of a nation but not formally annexed by it”
- ^ 『Seasar2で学ぶ DIとAOP アスペクト指向によるJava開発』技術評論社、2006年8月9日。
- ^ 『オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方』技術評論社、2016年9月2日。
- ^ a b c d “Java開発を変える最新の設計思想「Dependency Injection(DI)」とは”. ITPro (2005年2月18日). 2014年2月20日閲覧。
- ^ “Java EE 6: Understanding Contexts and Dependency Injection (CDI), Part 1”. オラクル (2010年5月25日). 2014年2月20日閲覧。
- ^ Microsoft.Extensions.DependencyInjection Namespace | Microsoft Docs