TypeScriptでクラスの依存注入(DI)を行う設計パターンを徹底解説!初心者でもわかる例付き
生徒
「先生、TypeScriptでクラス同士をうまく組み合わせて使う方法ってありますか?」
先生
「とても良い質問だね。プログラムの世界では、クラスが別のクラスを必要とすることがあるんだ。例えば、自動車クラスがエンジンクラスを必要とするようにね。」
生徒
「なるほど!でも、それをどうやって書くんですか?」
先生
「そのときに役立つのが『依存注入(Dependency Injection、略してDI)』という設計パターンなんだ。仕組みをわかりやすく解説するから、一緒に学んでいこう!」
1. 依存注入(DI)とは?
まず、依存注入とは何かをイメージで考えてみましょう。例えば、車(クラス)が動くためにはエンジン(別のクラス)が必要です。もし車の設計図の中で「この車は必ずガソリンエンジンを使う」と書いてしまうと、電気エンジンに変えたいときに全部作り直さないといけません。
そこで役立つのが「依存注入(DI)」です。これは、必要な部品(依存するクラス)を外から渡してあげる仕組みです。そうすることで、車クラスを作るときに「どのエンジンを使うか」を自由に差し替えられるようになります。
2. 依存注入を使わない場合
まずは、依存注入を使わない例を見てみましょう。車クラスの中で直接エンジンを作ってしまっています。
class Engine {
start() {
console.log("エンジン始動");
}
}
class Car {
private engine: Engine;
constructor() {
this.engine = new Engine(); // ここで直接作ってしまっている
}
drive() {
this.engine.start();
console.log("車が走り出しました");
}
}
const car = new Car();
car.drive();
エンジン始動
車が走り出しました
このコードでも動きますが、CarクラスがEngineにがっちり結びついているので、別の種類のエンジンに変えたいときに柔軟性がありません。
3. 依存注入を使った場合
次に依存注入を使った書き方を見てみましょう。エンジンは外から渡すように変更します。
class Engine {
start() {
console.log("エンジン始動");
}
}
class Car {
private engine: Engine;
constructor(engine: Engine) {
this.engine = engine; // 外から注入(渡す)
}
drive() {
this.engine.start();
console.log("車が走り出しました");
}
}
const engine = new Engine();
const car = new Car(engine); // ここで依存注入
car.drive();
エンジン始動
車が走り出しました
これで、Carクラスは「エンジンを持っている」という事実だけ知っていて、具体的にどんなエンジンなのかは外から決めることができます。
4. インターフェースを使った依存注入
さらに柔軟にするために、インターフェースを使うことができます。インターフェースとは「仕様書」のようなもので、「この機能があることだけ保証するよ」と約束するものです。
例えば、ガソリンエンジンと電気エンジンの両方を作ってみます。
interface IEngine {
start(): void;
}
class GasEngine implements IEngine {
start() {
console.log("ガソリンエンジン始動");
}
}
class ElectricEngine implements IEngine {
start() {
console.log("電気エンジン始動");
}
}
class Car {
private engine: IEngine;
constructor(engine: IEngine) {
this.engine = engine;
}
drive() {
this.engine.start();
console.log("車が走り出しました");
}
}
const gasCar = new Car(new GasEngine());
gasCar.drive();
const electricCar = new Car(new ElectricEngine());
electricCar.drive();
ガソリンエンジン始動
車が走り出しました
電気エンジン始動
車が走り出しました
このようにインターフェースを使うことで、Carクラスは「エンジンの種類」を全く気にせず、自由に切り替えることができます。
5. 依存注入を使うメリット
依存注入を使うと、以下のようなメリットがあります。
- 柔軟性が高い:エンジンをガソリンから電気に切り替えてもCarクラスを修正する必要がない。
- テストがしやすい:テスト用のダミーエンジンを注入して、実際に車を走らせなくても確認できる。
- 拡張性がある:将来、新しい種類のエンジンを作ってもCarクラスはそのまま使える。
つまり、依存注入(DI)は「部品を外から渡すことで、クラスの自由度を上げる設計パターン」なのです。
まとめ
TypeScriptで依存注入(DI)という設計パターンを使うことで、クラス同士の結びつきを弱め、より柔軟で拡張しやすいプログラムを構築できるという点が明確に理解できました。依存関係をクラス内部で作り込んでしまうと、変更のたびに多くのコードを修正する必要があり、テストもしづらくなります。しかし、依存注入を使うと、必要な部品を外から渡せるようになるため、クラスそのものの責務が整理され、差し替えや機能変更にも強い構造を作ることができます。特に、インターフェースと組み合わせて使うことで設計そのものを標準化でき、複数のパーツを使い分ける場面で大きな効果を発揮します。 依存注入の基本は「必要なものを自分で作らずに、外から渡してもらう」という考え方です。これによって、クラスは「どのような部品か」ではなく、「その部品が何ができるか」に注目するようになり、より抽象的で再利用しやすい構造になります。実際の開発では、データベース接続、ログ出力、外部API呼び出しなど多くの部分で依存注入が活用され、規模が大きいシステムであればあるほどDIの重要性が増していきます。特にテストのしやすさやメンテナンス性の向上は、長期的な開発で大きな違いを生みます。 ここでは、依存注入の理解をさらに深めるために、例として「通知サービス」を使ったより実践的なサンプルコードを用意しました。メール通知とライン通知など、複数の通知方法を切り替えられる設計はDIの本質をつかむのに非常に適しています。
サンプルプログラム:通知サービスの依存注入
interface INotifier {
notify(message: string): void;
}
class MailNotifier implements INotifier {
notify(message: string) {
console.log("メール通知:" + message);
}
}
class LineNotifier implements INotifier {
notify(message: string) {
console.log("LINE通知:" + message);
}
}
class UserService {
private notifier: INotifier;
constructor(notifier: INotifier) {
this.notifier = notifier;
}
sendWelcome(user: string) {
this.notifier.notify(user + "さん、ようこそ!");
}
}
const mailService = new UserService(new MailNotifier());
mailService.sendWelcome("太郎");
const lineService = new UserService(new LineNotifier());
lineService.sendWelcome("花子");
このサンプルでは、通知手段である MailNotifier と LineNotifier をインターフェース INotifier を通して統一し、UserService に外から渡す構造になっています。UserService の内部では「通知する」という仕様だけに注目しており、メールで通知するのか、ラインで通知するのかという具体的な実装は一切知りません。このように、依存注入を取り入れることで、UserService はどんな通知方法にも対応可能な柔軟なクラスとなり、将来的に新しい通知手段を追加する際も簡単に拡張できます。 この設計思想は、TypeScriptだけでなく多くのオブジェクト指向言語で用いられている根本的な考え方であり、規模の大小に関わらずさまざまなシステムに適用できます。依存注入という仕組みを理解することで、設計の幅が広がり、開発の質を大きく向上させることができます。
生徒
「今日の例を見て、依存注入ってクラスを自由に組み替えられる仕組みだとよくわかりました!」
先生
「そうだね。クラス同士の結びつきを弱めることで、変更にも強く、テストしやすいプログラムになるんだよ。」
生徒
「インターフェースと組み合わせると、もっと柔軟に設計できるのが面白かったです。」
先生
「依存注入は大規模開発でも必ず登場する考え方だから、今のうちにしっかり身につけておくと必ず役に立つよ。」
生徒
「はい!これからクラスを作るときは、内部で何でも作るのではなく、外から渡せる設計を意識してみます!」