概要
- ペーペーエンジニアの覚書
- DI/DIコンテナについていまいち分かっていなかったので学びを深める
- TypeScriptでの軽量DI実装まで
DI/DIコンテナを一から学んでみた
今回のテーマですが、「DI/DIコンテナを一から学んでみた」です。
なぜこのテーマにしたかですが、Clean Architectureで依存関係逆転の原則を本当にキレイに実装しようとすると、このDIの必要性が身に染みたからです。
解説だけだと面白くないので、TypeScriptでの軽量DI実装までやっていきます。
DIとは?
- デザインパターンの1つ
- Dependency Injection(依存性の注入)
- 依存性は性質ではなく、依存対象はインスタンス
- あるクラスが使用する他のクラスのインスタンスをコンストラクタなどを通じて外部から渡すようなクラス設計
- 一つの部品だけで開発・テストができ効率的
まず、DIとは?ですが、
DI自体は関心の分離を実現するためのデザインパターンの1つです。
そして、DIは「Dependency Injection」の略で、直訳すると「依存性の注入」です。
この日本語訳が意図が伝わりづらく混乱のもとになってしまいますが、依存性は性質ではなく、
依存対象を意味しており、注入される依存対象はオブジェクトのインスタンスです。
つまり、依存対象となるオブジェクトのインスタンスを外部から挿入するというデザインパターンです。
端的に申し上げると、「あるクラスが使用する他のクラスのインスタンスをコンストラクタなどを通じて外部から注入ようなクラス設計」になります。
DIを実現すると疎結合になるため、一つの部品だけで開発・テストができるようになり、開発効率が上がります。
このDIを内包した有名どころでは
Spring/Angular
などがあります。
DIコンテナとは?
- DIの実現をお手伝いするためのフレームワーク
- コンテナ = 容器、入れ物
- すごく簡単にいうと、「hogeにこのクラスのオブジェクトを注入して」という設定を書いておくと自動でインスタンスを渡してくれる
- 通常インスタンスを生成すると 直接 new するので 至るところに依存が生まれてしてしまう、見通しが悪くなるのを改善させる
続いては、DIコンテナについてです。
この点は非常に混乱しやすい点ですが、先程のDIとDIコンテナは別物です。
ここを混同して認識すると、混乱の元凶となってしまいますので、
区別が必要です。
では、何が違うのかですが、
DIコンテナは、DIという依存性の注入をお手伝いするためのフレームワークであるという点です。
つまり、DIコンテナがなくともDIは実現できます。
そして、DIコンテナの「コンテナ」とはそのまま、容器/入れ物という意味で、
どこにも依存していないプカプカ浮いた状態のインスタンスを入れておく容器になります。
呼び出し元から、このインスタンスが欲しい、とコールするだけでその時点でインスタンスを自動で渡してくれます。
これがなぜ素晴らしいかというと、通常インスタンスを生成すると 直接 new する必要があるので、
至るところに依存関係が生まれてしまい、インスタンスの量がどんどん増えると見通しも悪くなるのですが、
DIコンテナがこれら課題を解決してくれます。
DIコンテナを図で表すと、このようになります。
上の図がDIコンテナを使用していないパターン、
下の図がDIコンテナを使用したパターンです。
DIコンテナ内にはどこにも依存していないプカプカ浮いたインスタンスが多数入っており、
これらを適宜取り出します。
何が嬉しいのか?
- 依存関係逆転の原則がキレイに実装できる
- 依存先の実装が終わらなくとも、import側の実装ができる
- 依存先が変更されても、import側を修正しなくてもよい
- 依存が引き剥がされているので、テスト時にmockを流すだけで良い
ではDIによるメリットとはなんでしょうか?
色々とありますが、ざっと挙げると以下の4点です。
1、依存関係逆転の原則がキレイに実装できます。
Interfaceへ依存させていくと通常の実装では限界があります。
2、依存先の実装が終わらなくとも、import側の実装ができる
疎結合になっているため、依存先の実装完了を待たずに、import側の実装が可能になります。
3、依存先が変更されても、import側を修正しなくても良い
こちらも疎結合になっているため、import側が依存先の変更による影響を受けなくなります。
4、依存が引き剥がされているので、テスト時にmockを流すだけで良い
テスト時にわざわざDBヘアクセスせずとも、DIによってmockへすり替えるだけでテスト可能になります。
TypeScriptにおける課題
- JSには型が存在しない
- 解決策:ライブラリ
- InversifyJS:最もスターが多いが、最近は活発ではない
- TSyringe:Microsoftが主導で開発、現在も継続して開発が進められている
- 解決策:自作
- decorator:依存と注入対象のコードになんらかのマーキングをして、それだけで自動的に依存関係の登録ができる
- Reflection:ES6から導入。JSの機能でインターセプトが可能な JavaScript 操作に対するメソッドを提供するビルトインオブジェクト
- reflect-metadata:consturcotr からの引数取得と、interface 経由で injection するための一時的に interface と実装との紐付けの保管
ここまでDI/DIコンテナによるメリットをお伝えいたしましたが、「DIって良さそう」と多少は感じて頂けたかなと思います。
しかし、TypeScriptで実現しようとすると課題が残ります。
DIは抽象に依存するようにinterfaceなどの型が重要になりますが、
TypeScriptは型検査時に使われるものでしかなく、JS自体は型が存在しないため、トランスパイルすると消えてしまいます。
そのため、TypeScriptでのDIコンテナは、interfaceに紐づいた詳細を見つけてインスタンスを注入することができません。
この解決策には2通りあります。
まず、ライブラリの活用です。
InversifyJS(インバーシフィJS):Git hubで最もスターが多くついていますが、最近は開発が活発ではありません
TSyringe(Tシリンジ):TypeScriptのシリンジ(注射器)という意味ですが、Microsoftが主導で開発しており、現在も継続して開発が進められている
これらライブラリを活用することが最も早い解決策になります。
そして2点目が自作です。
もしDIコンテナを自作するとなると、大きな課題が残ります。
DIコンテナは依存と注入対象のコードになんらかのマーキングをして、
それだけで自動的に依存関係の登録ができるようにしなければいけないのですが、
その実現方法として decorator があります。
decorator もデザインパターンの1つになり、
decorator での依存登録方法は、InversifyJS(インバーシフィJS)やTSyringe(Tシリンジ)、JavaのSpring等でも採用・推奨されている方法のようです。
このdecoratorは最終的にはただの関数ですが、引数が何へ当てるのかによって異なります。
例えばクラス全体へのdecoratorだと、constructorとなり、
メソッドだと、1つ目がprotType、2つ目がmethodName、3つ目がPropertyDescriptor
といった具合です。
そしてこれらをdecorator factoryというもので手を加えてからreturnすることも可能です。
こういった引数が取れたり手を加えれることからも、
必ず必要という訳ではないですが、DIコンテナとdecoratorは相性がとても良いです。
続いてのReflection は ES6から導入されており、JavaScript エンジンが内部で使用している汎用的な関数(内部メソッド)を格納しているオブジェクトです。
ES5までは内部メソッドをコードから明示的に呼び出すことはできませんでしたが、Reflectionオブジェクトを通して呼び出す事ができます。
例えば、constructなどはその一例です。
しかし、これでも constructor の引数の情報を引っ張ってくることはできません。
そこでこの Reflectを拡張してくれるのが、次のreflect-metadataです。
reflect-metadata はReflectionを拡張することにより、より便利な機能を呼ぶ事ができます。
Reflect.getMetadata(designKey, target)
その一つにgetMetadataというメソッドがあり、これはtarget(つまりclass や function)が持つ情報を取得することができます。
どのような情報を取得できるかは designKey で指定でき、それぞれ次のような情報が取得できます。
"design:type" = 引数の型
"design:paramtypes" = 引数の型の配列
"design:returntype" = 戻り値の型
これらにより、型の恩恵を存分に受けながら記述することが可能となります。
ただし、今回は学びを深めるために必要最低限の機能しか持たない、
シンプルで軽量なDIを自作してみたいと思います。
はじめに断っておきますが、DIコンテナの自作はハードルが高いため今回はDIのみに焦点を当てます。
自作軽量DIに挑戦
では自作軽量DIを作成してみましたので、そちらの解説を行ってまいります。
全体のアーキテクチャは図のようになります。
メインはDIを理解するという趣旨のため、ライトなレイヤーにしています。
簡単に左から解説いたしますと、外部のユーザーがUIを操作して、
ApplicationレイヤーにClean Architecture でいう所のUse Case と 具象のInteractor があります。
そして、Dataレイヤーの所に永続化のRepositoryがあり、
そして、Infrastructureがあり、そこから外部のDBと接続します。
また、Domainが最上位レベルで一番安定した場所にいます。
Clean ArchitecturではEnterprise Business Rules層で所謂Entityに該当します。
DDDのEntityとは別物です。
Clean Architecture Like に 各レイヤーを保護するため、レイヤーを跨ぐ依存は全て依存関係逆転の原則に則っています。
また、Repositoryも抽象のClientに依存しているため、mock DataとDBを容易に切り替える事ができます。
今回の実装で npm run local と叩くと expressが起動しmock Dataが帰ってくるようになっています。
それぞれ、抽象に依存しているため、通常であればどこかで new しなければいけませんが、
今回はApplicationレイヤーでその解消をしたいと思います。
実際のコード: Repository
// interfaces\repository\user\IUserRepository.ts
import { User } from '@/domain/models/users/user';
import { Id } from '@/domain/models/users/vo';
export default interface IUserRepository {
fetchAll(): Promise<User[]>;
fetch(id: Id): Promise<User>;
}
// interfaces\repository\user\index.ts
import { User } from '@/domain/models/users/user';
import { Id } from '@/domain/models/users/vo';
import { API, USERS } from '@/infrastructure/Path';
import IClient from '@/infrastructure/provider/IClient';
import IUserRepository from '@/interfaces/repository/user/IUserRepository';
export class UserRepository implements IUserRepository {
constructor(private readonly _client: IClient) {}
async fetchAll(): Promise<User[]> {
const { data } = await this._client.get<User[]>(API + USERS);
return data.data;
}
async fetch(id: Id): Promise<User> {
const { data } = await this._client.get<User>(API + `${USERS}/${id}`);
return data.data;
}
}
続いては、実際のコードを見て行きます。
まずRepositoryですが、先程のアーキテクチャ図通りです。
constructor で 注入されるClientですが抽象に依存しています。
これにより、mockとの差し替えが容易になっています。
ごくごく普通の実装で特段何もありません。
実際のコード: Application
// application\users\useCase\IFetchAll.ts
import { User } from '@/domain/models/users/user';
export interface IFetchAll {
execute(): Promise<User[]>;
}
// application\users\interactors\fetchAll.ts
import { IFetchAll } from '@/application/users/useCase/IFetchAll';
import { User } from '@/domain/models/users/user';
import IUserRepository from '@/interfaces/repository/user/IUserRepository';
export class FetchAll implements IFetchAll {
constructor(private readonly userRepository: IUserRepository) {}
execute(): Promise<User[]> {
return this.userRepository.fetchAll();
}
}
続いては、Applicationです。
Clean ArchitectureではSOLID原則の1つに「インターフェイス分離の原則」があり、
使用していないものへの依存はしないという原則に則り、
このように1メソッドにつき、1インターフェイスとしています。
そして、こちらでも画像の通り、抽象に依存しています。
ここまでは特に問題ないのですが、このコードを実際に動くようなコードにしていくと問題が発生します。
それは、せっかく依存性をより上位にのみ向けて、制御の流れと依存方向を制御したのに、
どこかでnewしなければならず、結局具象に依存してしまうという点です。
そこでDIが効果を発揮します。
自作軽量DIのコード
// di\index.ts
import { Fetch, FetchAll } from '@/application/users/interactors';
import httpClientFactory from '@/infrastructure/HttpClientFactory';
import IClient from '@/infrastructure/provider/IClient';
import { UserRepository } from '@/interfaces/repository/user';
// Discriminated Union で補完が効くように
type KeyType = 'fetchAll' | 'fetch';
class DI {
constructor(private readonly _client: IClient) {}
// Function Overloading
getInstance(type: 'fetchAll'): FetchAll;
getInstance(type: 'fetch'): Fetch;
getInstance(type: KeyType) {
// Discriminated Union で補完が効く
if (type === 'fetchAll') {
const repository = new UserRepository(this._client);
return new FetchAll(repository);
}
if (type === 'fetch') {
const repository = new UserRepository(this._client);
return new Fetch(repository);
}
}
}
const di = new DI(httpClientFactory.getClient());
export default di;
実際に私がやってみたコードは画像のようになります。
ご覧いただいているように new が 登場せず依存関係が綺麗になっています。
DIを使わないとL7でnew をしなければならず強制的に依存が生まれてしまいますが、
getInstanceというメソッドを作りそこから欲しいインスタンスだけを渡すようにしています。
L6に記載しているDiscriminated UnionとはTypeScriptの手法の1つなのですが、
これにより、typeの後のstringにも型補完が効き、予測で候補が出てきます。
更にconst で受けているuseCaseという変数も、ちゃんとFetchAllというInstanceとして認識できているため、
L8のawait useCase.でも補完が効きます。
実際にはメソッド名を知る必要はないので、executeのみで良いのですが、しっかり型の恩恵を受けられている状態です。
実際のコード:newするところ
// application\users\index.ts
import di from '@/di';
import { User } from '@/domain/models/users/user';
import { Id } from '@/domain/models/users/vo';
class UserUseCase {
async findAll(): Promise<User[]> {
// Discriminated Union なので補完が効く
const useCase = di.getInstance('fetchAll');
const result = await useCase.execute();
return result;
}
async find(id: Id): Promise<User> {
// Discriminated Union なので補完が効く
const useCase = di.getInstance('fetch');
const result = await useCase.execute(id);
return result;
}
}
const user = new UserUseCase();
export default user;
では、どのようにインスタンスを返しているかですが、左側画像のようになります。
このDIクラスへ汚れ役を押し付けています。
正直あまりエレガントではないので、納得いってませんが、
L7でDiscriminated Union で補完が効くようにtypeの定義をしています。
このDiscriminated Union はL21の条件分岐の際にも補完が効きますし、
先程申し上げたように、getInstanceを実行する際にも補完が効きます。
こちらの定義はenumのような列挙型を使用すれば、よりタイプセーフになると思います。
そして、どのtypeがきたらどの型を返すのかという、Function OverloadingをL17で行っています。
これにより、「typeがfetchAll」の時にFetchAllという型が返り値として認識されるため、型の補完が効きます。
こちらをFunction OverloadingにせずにGenericsの方がキレイに実装できると思い右側の画像のようにやってみたのですが、
あまりキレイにできませんでした。
一応このコードで型の恩恵を受けつつ、newを隠蔽する事ができました。
しかし、このコードにもやはり課題が残ります。
課題
- DIするInstanceが増えるとDIクラスが巨大になる
- import がドンドン増える
- Discriminated Unionなので、typeがドンドン増える
- Function Overloadingなので、ドンドン増える
- 全てのInstanceがDIされている訳ではない
- Nuxtで実装したので、次はpulaginへ差し込みたい
まず、DIするInstanceが増えるとドンドンDIクラスが巨大になります。
それは、
importがドンドン増える、
Discriminated Unionなので、typeをドンドン追加しないといけない、
Function Overloadingなので、オーバーロードをドンドン追加しかければいけない。
また、今回のアーキテクチャ的では、DIすべき箇所は一箇所でしたが、
完全に全てのInstanceがDIされる訳ではありません。
今回 new を隠蔽することに焦点を当てて軽量DI作成いたしましたが、
やはりDIコンテナがあればもっとキレイに実装可能だと感じました。
そして、DIコンテナをキレイに実装しようとすると、decoratorやReflection-metadataが相性が良く
TypeScriptには標準でdecoratorがサポートされているため選択肢の一つだなと思いました。
こちらについては、個人的にdecoratorやReflectionを使用したDIコンテナをヒッソリといつか自作してみようと思っています。
今回の実装でも、かなり小規模であれば、ある程度はいけそな気もします。
大規模向けにはやはりDIライブラリの活用か、高スキルを持った方が開発される方が良いと思います。
まとめ
- DIとは?
- デザインパターンの1つ
- Dependency Injection(依存性の注入)
- 依存性は性質ではなく、依存対象はインスタンス
- DIコンテナとは?
- DIの実現をお手伝いするためのフレームワーク
- コンテナ = 入れ物
- 何が嬉しいの?
- 依存関係逆転の原則がキレイに実装できる
- 依存先の実装が終わらなくとも、import側の実装ができる
- 依存先が変更されても、import側を修正しなくてもよい
- 依存が引き剥がされているので、テストやlocal開発時にmockを流すだけで良い
- 軽量DI自作してみて
- 思ったより型補完効いたけど、課題が残った
- 色々身になった