Note
- 私は韓国でプログラミングと日本語を勉強している学生です
- 日本語がまだ不慣れなため、翻訳には機械翻訳を使用しました。もし不自然な点や間違いがありましたら、ご指摘いただけると幸いです
ファクトリーメソッドパターンの限界
前回の記事でファクトリーメソッドパターンについて整理した際、「誰」が new を通じてオブジェクトを生成するのかという問題がありました。
前回の内容を振り返ると、
- 結局、結合が移動しただけであり、解決されたわけではない
// どこかで結局呼び出される
map.put("サーモン", new SalmonRestaurant());
新しいメニューが追加されると、結局、開発者が RestaurantMap クラスを開いて static ブロックの中に直接 new を呼び出すコードを追加しなければなりません。
つまり、ファクトリーをファクトリーでラップし、いくら抽象化を行っても、プログラムのどこかでは必ず new を通じてオブジェクトを生成しなければなりません。
この問題を解決する方法こそが、DI (Dependency Injection) コンテナ です。
本日は、DIコンテナについて詳しく整理し、Springではどのようになっているか、一度確認してみたいと思います。
DIとは?
Dependency Injection(依存性の注入)の略で、
自分が必要とする依存性を直接生成せず、外部から受け取る設計パターンのことです。
DIのないコード:密結合(Tight Coupling)
// DIのないコード
public class SalmonRestaurant {
private SalmonChef chef;
public SalmonRestaurant() {
// [問題点] 依存するオブジェクト(Chef)を自ら直接生成している(密結合)
// もしTunaChefに変更したい場合、このコードを修正しなければならない
this.chef = new SalmonChef();
}
public void serve() {
chef.cook();
}
}
上記のコードの問題点は、RestaurantがChefを直接 new で生成しているという点です。 メニューを変更するには内部コードを修正する必要があるため、これは強い依存性、つまり密結合を意味します
DIを適用したコード:疎結合(Loose Coupling)
// DIありのコード
public class SalmonRestaurant {
// 具体的なクラス(SalmonChef)ではなく、インターフェース(Chef)に依存
private Chef chef;
// [解決] コンストラクタを通じて外部から依存性を注入される(Constructor Injection)
public SalmonRestaurant(Chef chef) {
this.chef = chef;
}
public void serve() {
chef.cook();
}
}
// --- 使用する場所(外部)---
// 外部でオブジェクトを生成して渡す
Chef myChef = new SalmonChef();
SalmonRestaurant restaurant = new SalmonRestaurant(myChef); // 注入!
これで Restaurant は具象クラスではなく、Chef インターフェースに依存するようになりました。
DIによって得られるメリット
- 結合度の低下
この部分は、ファクトリーメソッドパターンのメリットで触れた内容と同じです。
すべてのデザインパターンは、拡張に対しては開いており、変更に対しては閉じているようにすることで、
ドメインを重点的に開発できるようにするための仕組みだからです。
public class SalmonRestaurant {
private SalmonChef chef = new SalmonChef();
// ...
}
上記のようなコードの場合、TunaChefに変更したければ、Restaurant コードを必ず修正しなければならないという問題が生じます。
// TunaChefに変更したい場合、SalmonRestaurantのコードを修正する必要がある
private TunaChef chef = new TunaChef(); // コードの変更が必要
public interface Chef {
void cook();
}
public class SalmonChef implements Chef {
public void cook() { System.out.println("サーモン料理"); }
}
public class TunaChef implements Chef {
public void cook() { System.out.println("マグロ料理"); }
}
public class SalmonRestaurant {
private Chef chef; // インターフェースに依存
public SalmonRestaurant(Chef chef) {
this.chef = chef;
}
public void serve() {
chef.cook();
}
}
しかし、次のようにDI(依存性の注入)を使用すれば、DIP(依存性逆転の原則)を実現し、結合度を下げ、拡張に対して柔軟になることができます。
- テストの容易性
これは先ほどのコードと説明から繋がります。
DIがないとテストを行うのが難しくなります。
例えば、Chefに直接メールを送信するようなビジネスロジックがあると仮정してみましょう。
public class SalmonRestaurant {
private SalmonChef chef = new SalmonChef(); // 直接生成
public void call() {
chef.call(1111-1111);
}
}
@Test
void calltest() {
SalmonRestaurant restaurant = new SalmonRestaurant();
restaurant.call(); // 実際に連絡が行ってしまう
}
次のように、コード内に外部依存性(このコードではシェフへの連絡でしたが、DB依存や外部API依存など)があると、テストコードを実行するたびに問題が発生するでしょう。
しかし、DIを活用すれば、Mockオブジェクトを簡単に注入できるため、テストを容易に行うことができます。
// DIありのコード
public class SalmonRestaurant {
private Chef chef;
public SalmonRestaurant(Chef chef) {
this.chef = chef;
}
public void call() {
chef.call(1111-1111);
}
}
@Test
void callTest() {
// モックのChefを注入
Chef mockChef = mock(Chef.class);
SalmonRestaurant restaurant = new SalmonRestaurant(mockChef);
restaurant.call(); // 実際に電話はかからない!
// call()が呼び出されたかだけを検証
verify(mockChef).call(1111-1111);
}
このように、依存性逆転を通じてモックオブジェクトを利用し、簡単にテストを行うことができます。
- 柔軟な構成 (OCP)
OCPの原則も、上記の説明と繋がっています。先ほどの結合度を下げたコードで見たように、拡張が容易になります。これについては先ほど説明しましたので、詳細な説明は省略します。
DIコンテナとは?
DIコンテナは、オブジェクトの生成、依存性の注入、ライフサイクル管理を自動的に行ってくれるフレームワーク、またはライブラリです。
以前のコードで見たように、コードのどこかでは必ず、オブジェクトの生成と注入を開発者が責任を持って行わなければなりませんでした。
// 開発者が直接すべて行わなければなりません
Chef chef = new SalmonChef();
SalmonRestaurant restaurant = new SalmonRestaurant(chef);
これを自動的に管理してくれるのが、DIコンテナです。
Javaでは、代表的なものとしてSpringが使われています。
@Component
public class SalmonChef implements Chef { }
@Component
public class SalmonRestaurant {
private final Chef chef;
public SalmonRestaurant(Chef chef) { // Springが自動で注入
this.chef = chef;
}
}
DIコンテナによって何が得られるのか?
Because dependency injection separates how objects are constructed from how they are used, it often diminishes the importance of the new keyword found in most object-oriented languages. Because the framework handles creating services, the programmer tends to only directly construct value objects which represents entities in the program's domain (such as an Employee object in a business app or an Order object in a shopping app).
ついに、開発者がオブジェクトの生成を逐一担当しなくても、DIコンテナがオブジェクトの生成・注入を担当してくれるため、開発者はビジネスロジックだけに集中できるようになりました。
このように、オブジェクトの生成と注入 +(管理)の責任を、開発者ではなくコンテナ(フレームワーク、ライブラリ)に委ねることを、まさに 制御の反転(IoC, Inversion of Control) と呼びます。
制御の反転 (IoC)
In software design, inversion of control (IoC) is a design principle in which custom-written portions of a computer program receive the flow of control from an external source (e.g. a framework). The term "inversion" is historical: a software architecture with this design "inverts" control as compared to procedural programming. In procedural programming, a program's custom code calls reusable libraries to take care of generic tasks, but with inversion of control, it is the external code or framework that is in control and calls the custom code.
この内容を簡単に整理すると、
IoCとは、開発者が作成したコードが外部(フレームワークなど)から制御の流れを受け取る設計原則です。
つまり、私がフレームワークを呼び出すのではなく、フレームワークが私のコードを呼び出すという意味です。
IoCとDIの関係
DIとIoCの違い
- IoCは抽象的な概念です
- DIはIoCを実現する具体的な方法の一つです
この用語の違いに関する説明は、DDDの父、マーティン・ファウラーのブログに記されています。
In the Java community there's been a rush of lightweight containers that help to assemble components from different projects into a cohesive application. Underlying these containers is a common pattern to how they perform the wiring, a concept they refer under the very generic name of “Inversion of Control”. In this article I dig into how this pattern works, under the more specific name of “Dependency Injection”, and contrast it with the Service Locator alternative. The choice between them is less important than the principle of separating configuration from use.
昔々、Javaコミュニティでは様々な軽量コンテナが登場しました。
これらのコンテナの基盤には、コンポーネント同士を連結する共通のパターンがあり、これを「Inversion of Control(制御の反転)」という非常に一般的な名前で呼んでいました。
マーティン・ファウラーはこの記事の中で、「Dependency Injection(依存性の注入)」という、より具体的な名前で掘り下げ、Service Locatorという代替案と比較しています。
何を(どのパターンを)選択するかということよりも、設定(configuration)と使用(use)を分離する原則の方がより重要です。
Service Locatorとは?(DIの代替案)
サービスを探す処理を中央で一括して行う方式のことです。
- DI(Dependency Injection)
public class SalmonRestaurant {
private final Chef chef;
// 外部から注入される(受動的)
public SalmonRestaurant(Chef chef) {
this.chef = chef;
}
}
- Service Locator
public class SalmonRestaurant {
private final Chef chef;
public SalmonRestaurant() {
// 自ら取得しに行く(能動的)
this.chef = ServiceLocator.getService(Chef.class);
}
}
この記事が書かれたのは2004年で、20年が経過した現在、Service Locator方式は管理が難しいためあまり使用されず、DIが主流となりました。
なぜなら、Service Locator方式では、依存性の流れを開発者が把握しづらいからです。
public SalmonRestaurant() {
// コードの中身を見て初めて、Chefに依存していることが分かる
this.chef = ServiceLocator.getService(Chef.class);
}
-
Chefが登録されていなくてもコンパイルは通りますが、実行時(ランタイム)に問題が発生します。 -
テスト時に、グローバルなServiceLocatorの状態を毎回設定しなければなりません。
このような問題などから、現代ではDIを通じた明確で宣言的な管理方式が主流となり、Springはこれを採用しています。
// Spring
@Component
public class Restaurant {
private final Chef chef;
// Springが自動的にSalmonChefを注入してくれます
public Restaurant(Chef chef) {
this.chef = chef;
}
public void serve() {
chef.cook();
}
}
DIコンテナの核心となる動作原理とは?
DIコンテナの核心となる動作原理は次の通りです。
-
設定 (Configuration)
- どの実装体を使用するかを設定
-
オブジェクト生成 (Creation)
- DIコンテナがオブジェクトを生成
-
依存関係注入 (Injection)
- 生成したオブジェクトを必要な場所に注入する
Javaでは、これを Java Reflection API を活用して、ランタイムに動的にオブジェクトを生成し、注入します。
次回の記事では、Springにおいて設定、オブジェクト生成、依存関係注入がどのように実装され、使用されているのかについて整理してみたいと思います。
