When
2017/02/07
At
自己紹介
ちきさん
(Tomohiro Noguchi)
Twitter/GitHub/Qiita: @ovrmrw
ただのSIer。
Angular Japan User Group (ng-japan)スタッフ。
アカウントIDの由来
- the day after tomorrow
- overmorrow(俗語)
- 略して
- ovrmrw
- 「先を見据えて」よりさらに先を、みたいな感じです。
(よく聞かれるので)
(ここからInversifyJSの基本の話)
===
GitHubリポジトリ ovrmrw/inversify-mock-example
モチベーション: ReactだってDIしたい
InversifyJS
- a library for Inversion of Control
- Angularのように
@injectable
とか@inject
とか書いてDependency Injection(依存性注入)できます。
事前準備 (TypeScriptの場合)
tsconfig.json
でデコレーターを有効にします。
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
}
なるべく最初の方でreflect-metadata
を読み込みます。(polyfill)
import 'reflect-metadata'
まずInjectするクラスを作ります。
DIコンテナの中でインスタンス化させるクラスは@injectable
デコレーターが必須です。
import { injectable } from 'inversify'
@injectable() // ★
export class Katana {
hit() {
return 'cut!'
}
}
@injectable() // ★
export class Shuriken {
throw() {
return 'hit!'
}
}
DIコンテナの定義を書きます。慣例としてファイル名はinversify.config.ts
とします。
import { Container } from 'inversify'
import { Katana, Shuriken } from './services'
const rootContainer = new Container()
rootContainer.bind(Katana).toSelf() // ★
rootContainer.bind(Shuriken).toSelf() // ★
export const container = rootContainer.createChild()
補足説明
container.bind(Katana).toSelf()
意味: Katana
トークンに対してKatana
クラスのインスタンスをInjectする。
===
モックする場合は?
container.bind(Katana).to(MockKatana)
意味: Katana
トークンに対してMockKatana
クラスのインスタンスをInjectする。
Ninja
クラスにKatana
,Shuriken
クラスをInjectしてみる。
@injectable()
class Ninja {
constructor(
@inject(Katana) private katana: Katana, // @inject(Katana)は省略可
@inject(Shuriken) private shuriken: Shuriken, // @inject(Shuriken)は省略可
) { }
fight() {
return this.katana.hit()
}
sneak() {
return this.shuriken.throw()
}
}
@injectable()
class MockKatana implements Katana {
hit() {
return 'cut! (mock)'
}
}
container.bind(Ninja).toSelf()
container.bind(Katana).to(MockKatana) // ★
const ninja = container.get(Ninja) // ★
console.log(ninja.fight()) // output: "cut! (mock)"
console.log(ninja.sneak()) // output: "hit!"
補足説明
constructor(
@inject(Katana) private katana: Katana, // @inject(Katana)は省略可
@inject(Shuriken) private shuriken: Shuriken, // @inject(Shuriken)は省略可
) { }
Ninja
クラスのconstructor
で
-
Katana
トークンにbindされたクラスをインスタンス化して変数katana
にInjectする。 -
Shuriken
トークンにbindされたクラスをインスタンス化して変数shuriken
にInjectする。
ということが行われています。
そして変数katana
にはKatana
クラスのインスタンスではなく、
container.bind(Katana).to(MockKatana)
上記の一行によってMockKatana
クラスのインスタンスがInjectされます。
もしこの行が無かったら、
rootContainer.bind(Katana).toSelf()
rootContainer
の定義が採用されてKatana
クラスがInjectされます。
図で説明 (1/2)
図で説明 (2/2)
- ChildContainerで上位のContainerの定義を上書きできる。
- ChildContainerでbindされたクラスが見つからなかったら上位のContainerを辿って探す。
さらに補足説明
Ninja
クラスのインスタンスを取得するときにnew Ninja()
のように書いてはいけません。
const ninja = container.get(Ninja)
上記のようにcontainer.get(Ninja)
と書くことでDIコンテナからNinja
クラスのインスタンスを取り出します。
依存性の解決はDIコンテナの中でよしなにやってくれるというわけです。
(ここからReactの話、つまり本題)
===
GitHubリポジトリ ovrmrw/meguroes-react-inversify-typescript
まずInjectする適当なクラスを作ります。
@injectable()
を付けるのがコツですね。
import { injectable } from 'inversify'
@injectable() // ★
export class Actions {
goo(): string {
return 'goo!'
}
choki(): string {
return 'choki!'
}
paa(): string {
return 'paa!'
}
}
DIコンテナの定義を書きます。
import { Container } from 'inversify'
import getDecorators from 'inversify-inject-decorators'
import { Actions } from './actions'
const container = new Container()
container.bind(Actions).toSelf() // ★
export const { lazyInject } = getDecorators(container) // ★
Actions
トークンにActions
クラス自身をbindします。
inversify-inject-decorators
というライブラリを使ってlazyInject
というデコレーターをexportしているのがポイントです。
Reactのコンポーネントを書きます。
ようやく今日一番話したかった@lazyInject(Actions) actions: Actions
が登場しました。
import { lazyInject } from './inversify.config'
import { Actions } from './actions'
export class App extends React.Component<{}, {}> {
@lazyInject(Actions) actions: Actions // ★ constructorですらない
goo(event): void {
this.setState({ janken: this.actions.goo() })
}
choki(event): void {
this.setState({ janken: this.actions.choki() })
}
paa(event): void {
this.setState({ janken: this.actions.paa() })
}
random(event): void {
const random = Math.random()
if (random > 0.66) {
this.goo(event)
} else if (random > 0.33) {
this.choki(event)
} else {
this.paa(event)
}
}
render() {
return (
<div>
<button onClick={(e) => this.goo(e)}>グー</button>
<button onClick={(e) => this.choki(e)}>チョキ</button>
<button onClick={(e) => this.paa(e)}>パー</button>
<button onClick={(e) => this.random(e)}>ランダム</button>
<h1>{this.state.janken}</h1>
</div>
)
}
}
(改行してもしなくてもどちらでも良い)
@lazyInject(Actions) actions: Actions
@lazyInject(Actions)
actions: Actions
意味: Actions
トークンにActions
クラスのインスタンスをInjectする。ただしインスタンス生成時ではなく実行時にInjectされる。
(追記)...と思ったけどconstructor
の中でthis.actions
を参照してもエラーにはならないのでインスタンス生成時にうまいことInjectしてるのかも。
Q: lazyである必要があるの?
A: 本来であればcontainer.get(App)
のようにInversifyJSがインスタンス生成のタイミングを握らなければならないが、Reactが握っているため仕方なく後からInjectする必要がある。
READMEにもこのように書いてあります。
Some frameworks and libraries take control over the creation of instances of a given class. For example, React takes control over the creation of instances of a given React component. This kind of frameworks and libraries prevent us from being able to use constructor injection and as a result they are not easy to integrate with InversifyJS.
いくつかのフレームワークとライブラリは、特定のクラスのインスタンスの作成を制御します。例えば、Reactは与えられたReactコンポーネントのインスタンスの作成を制御します。この種のフレームワークやライブラリは、コンストラクタインジェクションを使用できないため、InversifyJSとの統合が容易ではありません。