Typescript 2.2.0 の Mixin を使ってDIしてみる
表題の通り。
今まではMixinがなかったので、泥臭く型チェックができない方法(文字列とか)でDIしていたが、
typescript 2.2.0からMixinに対応したので、DIを考察する。
Dependency Injectionとは
Dependency Injectionとはその名の通り依存性を外から注入する仕組み。
なにが嬉しいかというと、クラス内部で依存しているはずのクラスを外部からコントロールできるようにすることで、
クラスの内部実装と外部への依存を疎結合にし、テストし易いクラスを作ることができる。
猿でも分かる! Dependency Injection: 依存性の注入 - Qiita
Inversion of Control コンテナと Dependency Injection パターン
DI for Typescript
さて、Typescriptで上述のDependency Injectionを実現するにはどういうパターンがあるか、検証してみたい。
public Dependency パターン
名前は勝手につけました。
その名の通りpublicプロパティに依存性を設定する。
実装
// deps1.ts export interface Deps1 { hello(): string; } export class Deps1Impl implements Deps1 { public hello() {return 'hello'} } // deps2.ts export interface Deps2 { world(): string; } export class Deps2Impl implements Deps2 { public world() {return 'world'} } // deps1+deps2-module.ts import { Deps1 } from './deps1'; import { Deps2 } from './deps2'; export interface Deps1Deps2Module { deps1: Deps1; deps2: Deps2; } // deps1+deps2-module-impl.ts import { Deps1Deps2Module } from './deps1+deps2-module'; import { Deps1Impl } from './deps1'; import { Deps2Impl } from './deps2'; type Constructor<T> = new(...args: any[]) => T; export function Deps1Deps2ModuleImpl<T extends Constructor<Deps1Deps2Module>>(Base: T) { return class extends Base { constructor(...a) { super(...a); this.deps1 = new Deps1Impl(); this.deps2 = new Deps2Impl(); } }; } // inject-target.ts import { Deps1 } from './deps1'; import { Deps2 } from './deps2'; import { Deps1Deps2Module } from './deps1+deps2-module'; export class InjectTarget implements Deps1Deps2Module { public deps1: Deps1; public deps2: Deps2; public greet() {return `${this.deps1.hello()} ${this.deps2.world()}`} } // injected.ts import { InjectTarget } from './inject-target' import { Deps1Deps2ModuleImpl } from './deps1+deps2-module-impl'; export class Injected extends Deps1Deps2ModuleImpl(InjectTarget) { } // main.ts import { Injected } from './injected'; const injected = new Injected(); console.log(injected.greet());
deps1.ts deps2.ts
これらは単純な依存クラス
deps1+deps2-module.ts
こちらはdeps1とdeps2を注入される側の規約を定義したinterfaceクラス
依存性を注入される側はこのインターフェースを実装する。
deps1+deps2-module-impl.ts
依存性注入を実行するMixin関数。
ここで実際に依存性の注入を行う。
inject-target.ts
依存性を注入されるクラス。
interface Deps1Deps2Module
をimplementsしている以外は、通常のクラスと変わりない。
injected.ts
ここでDeps1Deps2ModuleImpl関数にInjectTargetクラスを渡したクラスを継承することで、
依存性が注入されるクラスを生成する。
main.ts
エントリーポイント
問題点
publicプロパティに実装するしか無いので、カプセル化に問題がある。
防御的なクラスが作りづらい。
method injection パターン
こちらは上記の問題を踏まえ、publicメソッドを使ってinjectionの実現をする。
実装
// deps1+deps2-module.ts import { Deps1 } from './deps1'; import { Deps2 } from './deps2'; export interface Dependencies { deps1: Deps1; deps2: Deps2; } export interface Deps1Deps2Module { __setDependencies(deps: Dependencies): void; } // deps1+deps2-module-impl.ts import { Deps1Deps2Module } from './deps1+deps2-module'; import { Deps1Impl } from './deps1'; import { Deps2Impl } from './deps2'; type Constructor<T> = new(...args: any[]) => T; export function Deps1Deps2ModuleImpl<T extends Constructor<Deps1Deps2Module>>(Base: T) { return class extends Base { constructor(...a) { super(...a); this.__setDependencies({deps1: new Deps1Impl(), deps2: new Deps2Impl()}); } }; } // inject-target.ts import { Deps1 } from './deps1'; import { Deps2 } from './deps2'; import { Deps1Deps2Module } from './deps1+deps2-module'; export class InjectTarget implements Deps1Deps2Module { private deps1: Deps1; private deps2: Deps2; public greet() {return `${this.deps1.hello()} ${this.deps2.world()}`} public __setDependencies({deps1, deps2}) { this.deps1 = deps1; this.deps2 = deps2; } }
変更点
deps1+deps2-module.ts
__setDependencies
というpublicなセッタメソッドを用意して、
外部から直接interfaceを触れられないようにした。
deps1+deps2-module-impl.ts
__setDependeciesに依存関係を渡すように修正
inject-target.ts
__setDependencies
を実装
問題点
__setDependencies
がpublicメソッドなのは変わらず。
考察
ほんとはconstructor injectionが型チェック付きでできれば良いのだが…
今回のMixinでは不可能なので、method injectionでやるのが一番良いのかなと思う。
元々はScalaのCakeパターンとかあのへんの感じでやりたかった(願望)
ただ、typescriptの貧弱な言語機能でもここまで頑張れるのがわかったので良かった。
とりあえず、文字列ベースのDIコンテナを使うよりはいいかもしれないので、一旦プロダクトで使ってみようと思う。
可能性を伸ばしていきたい。
伸びしろですねぇ!
今回の実装はこちらにありまぁーす