背景
Vue.jsを用いたアプリケーションでDI(Dependency Injection, 依存性の注入)を行いたくなった時、どうすればよいでしょうか?
まず、Vue.js向けのDIライブラリを探してみます。Awesome Vue をみてみると、2つライブラリがあることがわかりました。
また、NPMで検索するとさらに多くのライブラリが見つかりました。
これらのライブラリの問題点は、それぞれのドキュメントを読んでもらうとわかるのですが、サービスロケータパターンになっていることです。つまり、VueコンポーネントがこれらのDIライブラリに依存してしまっている、と言うことです。(参考記事: やはりあなた方のDependency Injectionはまちがっている。 — A Day in Serenity (Reloaded) — PHP, FuelPHP, Linux or something))
サービスロケータパターンだけは避けたい。
そこで現状どうしているかというと、Vue.js の provide/inject を利用しています。これは React における provide/context
APIと同等なものです。
具体的には、ルートコンポーネントの provide
に、注入したいオブジェクトをまとめて登録します。そして、それらを利用したい子孫コンポーネントにおいて inject
を用いてそのオブジェクトを取り出しています。
普段ES2015のclassシンタックスで記述しているので以下のようなコードになります。
import { SERVICE_A, ServiceA } from './services/ServicesA'
import { SERVICE_B, ServiceB } from './services/ServicesB'
import { IdbUserRepository } from './infra'
const repository = new IdbUserRepository()
export const provide = {
[SERVICE_A]: new ServiceA(repository),
[SERVICE_B]: new ServiceB(repository),
}
import { provide } from './provide'
import App from './App.vue'
new Vue({
provide,
render: h => h(App),
})
<script lang="ts">
import { Vue, Component, Inject } from 'vue-property-decorator'
@Component
export class AppComponent extends Vue {
@Inject(SERVICE_A) serviceA!: ServiceA
@Inject(SERVICE_B) serviceB!: ServiceB
myMethod() {
this.serviceA
this.serviceB
}
}
</script>
しかしながら、この手法では自分自身で依存関係を解決しなければなりません(上記の例では2つのサービスにリポジトリを渡すだけですが、実際には大量の登場人物がいます)。
そこで、今回は自分自身でDIライブラリを作って、これらの課題を解決してみようと試みました。
目的
Vue.jsの provide/inject
を利用するDIライブラリを作成する。
方法
とりあえずTypeScriptとJestを用意しました。
DIライブラリ
container
の実装
まず、DIで修飾するクラスを登録するオブジェクトを作りました。
container
と言う名前を付けました。シングルトンオブジェクトです。
このオブジェクトは、管理対象のクラスと、そのコンストラクタ引数の情報を保持します。
デコレータ関数の実装
次に2つのデコレータ関数を作りました。
-
@Injectable()
クラスデコレータ -
@Inject(key: string | symbol)
パラメータデコレータ
@Injectable
デコレータは対象となるクラスを前述のコンテナに登録する役割があります。また @Inject
デコレータは key
引数をキーとして、対象となるクラスの引数をコンテナへ登録します。
(ただし、現状の実装では @Inject
は @Injectable
と同じ役割を担っているので、正直無くてもいいです。)
これらのデコレータは、以下のように使用します。
export interface Keyboard {
onPress$: Observable<number>
}
export const KEYBOARD = Symbol()
export interface Monitor {
showResult(result: Result): void
}
export const MONITOR = Symbol()
interface Result {
// ...
}
@Injectable()
export class Computer {
constructor(
@Inject(KEYBOARD) private keyboard: Keyboard,
@Inject(MONITOR) private monitor: Monitor,
) {
keyboard.onPress$
.pipe(map(keyCode => this.handleEvent(keyCode)))
.subscribe(result => monitor.showResult(result))
}
handleEvent(keyCode: number): Result {
// ...
return result
}
}
export const COMPUTER = 'COMPUTER'
Provider
の実装
最後に、 Vue.jsのコンポーネントへオブジェクトを注入する Provider
クラスを作りました。
このクラスのインスタンスは、コンポーネントへ注入したいオブジェクトを生成する役割があります。
- オブジェクトを注入するのに必要なクラスや注入したいオブジェクトを登録します。
- Vue.jsへ渡せるようなオブジェクトを生成します。
@Injectable()
class MyKeyboard implements Keyboard {
onPress$ = new Subject<number>()
// ...
}
const monitor: Monitor {
showResult(result) {
// ...
}
}
const provider = new Provider()
provider.bindConstructor(COMPUTER, Computer)
provider.bindConstructor(KEYBOARD, MyKeyboard)
provider.bindeObject(MONITOR, monitor)
new Vue ({
provide: provider.provide() // <- これが一番実現したかったこと
})
すると、Vue.jsのコンポーネントの中で、以下のようにオブジェクトを利用できます。
import { Vue, Component, Inject } from 'vue-property-decorator'
@Component
export class AppComponent {
@Inject(COMPUTER) computer!: Computer // <- オブジェクトを取り出すことができている
say() {
this.computer
}
}
DIライブラリを用いたサンプルアプリケーション
本日12月4日は、『BanG Dream!』に登場するキャラクター「花園たえ」の誕生日であります。したがって彼女の誕生日をお祝いするアプリケーションを作ろうと思います。
前回と比較すると、今回作成したDIライブラリを利用していること、Vuexを使わなくなったこと、 .vue
ファイルを利用していること、などの変更点があります。
結果
成果物はこちらのリポジトリです。
考察
DIライブラリ
このライブラリで一番重要な機能は、provide
プロパティにオブジェクトを渡す Provider#provide()
です。その為、Inversify.jsの様な広く使われているライブラリを基盤にして、 #provide()
メソッドの実装に注力することが、今後のNPMへの公開を見据えると良い判断になるかと思いました。。
サンプルアプリケーション
よくできたと思います。
見事にサービスロケータを回避できていることがわかります。
感想
ディディディディDI
ディディディディDI
DI DI
「頑張ってね」「DIです」