TypeScriptで依存関係を実装することを考えたとき、最初にアドバイスされたのはinversifyでした 。 私はこれと、Service Locatorパターンを実装する他のライブラリを見て、独自のtypedinを作成しました 。
しかし、typedin 2.0に取り組んだとき、ライブラリがまったく必要ないことがようやくわかりました。 TypeScriptには必要なものがすべて揃っています。
サービスロケーターはアンチパターンです
Service Locatorがアンチパターンであることは長い間知られています。 まず、暗黙的な依存関係が作成されるためです。 サービスコンテナをクラスに渡すだけで、クラスコードでサービスを任意に取得する場合、そのようなクラスの依存関係を見つける唯一の方法は、そのコードを調べることです。
// inversify var ninja = kernel.get<INinja>("INinja");
もちろん、プロパティを介して依存関係が導入されると、この状況はわずかに改善されます。 たとえば、これはtypedin
で行われる方法typedin
(逆変換用のデコレータもあります)。
class SomeComponent { @inject logService: ILogService; }
このプロパティを宣言することにより、クラスインターフェイスでその依存関係を宣言します。 しかし、これはまだ悪いです。必要な依存関係を渡さずにクラスのインスタンスを安全に作成し、実行時エラーを取得できます。 IDEは、クラスを正しく使用する方法を教えません。
代わりに、ドキュメントを自分で調べて調べる必要があります。 しかし、すべての困難を克服し、適切なコードを記述したとしましょう。 ただし、新しい機能を追加するときに誰かがクラスに別のサービスを追加しても、コンパイラはこれについて警告しません。 目的のサービスを転送しないため、コードは単にランタイムをドロップします。
これらすべての理由から、依存関係を実装する最良の方法は、 コンポジションルートと組み合わせたコンストラクター注入です。
class SomeComponent { constrcutor(private logService: ILogService) { } }
コンストラクター注入の難しさ
コンストラクターを介した依存性注入には、上記のすべての欠点がありません。 依存関係を明示的に宣言するため、ユーザーは必要なサービスを渡さずにクラスのインスタンスを作成することはできません。 この場合、コンパイラーはコードを完全に制御し、すぐにエラーを報告します。 ただし、このアプローチは「生の」形式ではかなり不便です。 クラスのインスタンスを作成するたびに、必要なすべてのサービスをクラスに転送する必要があります。
var some = new SomeComponent(logService)
また、コンポーネントのツリーがある場合は、依存関係を渡すためのコードをチェーン全体に記述する必要があります。
class SomeWrapperComponent { constructor(private logService: ILogService) { var some = new SomeComponent(logService) } }
SomeComponent
のサービスのリストを変更する場合、 SomeWrapperComponent
のコードを変更してから、それを使用する全員を変更するSomeComponent
がありSomeComponent
。 これは、サービスの数がかなり多くなると特に悲しくなります。
ただし、 Angularが示したように、TypeScriptのデコレーターのおかげで、コンストラクターオプションにリストされた依存関係を自動的に挿入できます。
// Angular @Injectable() export class HeroService { constructor(private logger: Logger) { } }
つまり、一方では、コンストラクターパラメーターで明示的に依存関係を宣言し、他方では、各コンポーネントにサービスを転送するためのボイラープレートの束を作成しません。 サービスは、コンポーネントツリーまたは親モジュールに自動的に配置されます。
ただし、このアプローチはReactで実装するには問題があります。 Reactコンポーネントのコンストラクターの類似物はprops
です。 つまり、Reactのコンストラクター注入は次のようになります。
render() { return <SomeComponent logService={this.logService} /> }
残念ながら、 props
は単なるインターフェイスであり、Angularのように、デコレータを使用して自動依存性注入を行うことはできません。
export interface SomeComponentProps { logger: Logger } export class SomeComponent extends React.Component<SomeComponentProps, {}> { }
これは単なるReactの問題ではありません。 多くのフレームワークでは、コンストラクターを介してコンポーネントの作成を制御しません。 たとえば、同じVueで。 実際、Angularでも、コンストラクタを介してコンポーネントを作成する人はいないため、コンポーネントもすべて関連しています。
TypeScriptネイティブ依存性注入
typedin v2.0で作業しながら、これらすべてをどのように組み合わせるかを長い間考えていました。 コンストラクターインジェクションのように、依存関係の転送の明示的な性質を保持したかったのですが、同時に定型の数を減らして、Reactとの互換性を持たせました。
徐々に、そのようなソリューションのプロトタイプが現れ始めました。 コードを段階的に改善し、タイプディンライブラリに何も残らなくなるまで、不要なものをすべて捨てました。 必要なものはすべてTypeScriptに既にあることが判明したため、この記事はtypedin v2.0であると言えます。
そのため、サービス広告の横に$Logger
ような広告を1つ追加するだけです。
export class Logger { log(msg: string) { console.info(msg); } } export type $Logger = { logger: Logger; };
別のサービスを追加して、さらに興味深いものにします。
export class LocalStorage { setItem(key: string, value: string) { localStorage.setItem(key, value); } getItem(key: string) { return localStorage.getItem(key); } } export type $LocalStorage = { localStorage: LocalStorage }
Logger
とLocalStorage
を必要とするコンポーネントを宣言します。
export interface SomeComponentProps { services: $Logger & $LocalStorage; } export class SomeComponent extends React.Component<SomeComponentProps, {}> { constructor(props) { super(props); // let habrGreeting = props.services.localStorage.getItem("Habrahabr"); props.services.logger.log("Native TypeScript DI! " + habrGreeting); ) }
依存性注入も必要とする別のサービスを宣言しましょう。
export class HeroService { constructor(private services: $Logger) { services.logger.log("Constructor injection is awesome!"); } }
それをすべてまとめることが残っています。 アプリケーションのある時点で、 コンポジションのルートパターンに従ってすべてのサービスを初期化します。
let logger = new Logger(); export var services = { logger: logger, localStorage: new LocalStorage(), heroService: new HeroService({ logger }) // ! };
これで、このオブジェクトをコンポーネントに渡すことができます。
render() { return <SomeComponent services={services} /> }
以上です! ボイラープレートなしの真のクリーンユニバーサルコンストラクター注入 !
すべての仕組み
型のこの &
TypeScriptが大好きです。 このすべてがとてもシンプルでエレガントに見えるのは彼のおかげです。 Logger
サービスを宣言するとき、さらに$Logger
型を宣言しました。 type
構成体と混同している場合、代替手段は次のようになります。
export interface $Logger { logger: Logger; }
文字通り、 Logger
変数にLogger
サービスを含むコンテナのインターフェースを宣言しています。 すべてのサービス- $LocalStorage
、 $HeroService
です。 コンポーネントでは、いくつかのサービスが必要なので、2つのインターフェイスを組み合わせるだけです。
services: $Logger & $LocalStorage;
この設計は次と同等です。
interface SomeComponentDependecies extends $Logger, $LocalStorage { logger: Logger; localStorage: LocalStorage; } services: SomeComponentDependecies;
つまり、 SomeComponent
コンポーネントには、 Logger
およびLocalStorage
を含むコンテナを渡す必要があるとLocalStorage
。 そしてそれだけです! 対応するコンテナがどのようにコンポーネントに転送され、どこから来て、どのように作成されるかはそれほど重要ではありません。 コンポジションルートの1か所で作成された一部のグローバルservices
オブジェクトをインポートできservices
。 親オブジェクトのチェーンを介してこのオブジェクトを渡すことができます。 オンデマンドで動的に作成できます。 それはすべて特定のアプリケーションの条件に依存します。
おわりに
InversifyJSには、約40のセクションからなる約100 kbのコードとドキュメントが含まれていますが、これらは理解しやすいものではありません。 それにもかかわらず、そのnpmパッケージは月に約10万回ダウンロードされ、多くのプラグインと拡張機能がそのために書かれています。 これから2つの結論を導き出すことができます。
- 依存性注入は、フロントエンドの世界で人気を集めています
- フロントエンドコミュニティは、Service Locatorがアンチパターンであることをまだ認識していません
つまり、いつものように、他の技術や言語から考えを考えずに取得します。 実際、依存関係の反転は単にパラメーターを渡すだけであり、これにライブラリは必要ありません。 これらのファクトリー 、 プロバイダー 、 バインダー 、 ハンドラー 、 循環依存などはすべて、リソースとそれらが与えるコードの複雑さの価値があると確信していますか?