この記事を読みました。すごくわかりやすい記事です。
以下のことがふんわりわかりました。
- Service Locator も DI も同じような課題を解決するためのもの
- Service Locator と DI コンテナは利用する目線だとほとんど同じもの
- DI の方がおすすめ
なんとなく理解したんですが、たぶん後輩に聞かれても説明できないです。
このモヤモヤを取り払うため、実際に TypeScript で書いてみることにしました。
何も考えずに書くと
class A {
exec(){
console.log('exec A')
const b = new B()
b.exec()
}
}
class B {
exec(){
console.log('exec B')
}
}
class App {
main(){
const a = new A()
a.exec()
}
}
const app = new App()
app.main()
App から A を呼び出し、A から B を呼び出しています。
A は B そのものに依存してしまっているので、テストしようにも B 以外で代替することができません。
また、A は呼び出されるたびに B をインスタンス化しているので、パフォーマンス上も問題があるかも。
インターフェースを取り入れる
class A {
exec(){
console.log('exec A')
// 型を追加
const b: IB = new B()
b.exec()
}
}
// 追加
interface IB {
exec: () => void
}
class B implements IB {
exec(){
console.log('exec B')
}
}
class App {
main(){
const a = new A()
a.exec()
}
}
const app = new App()
app.main()
実際に動作自体は何も変わってないです。
ただ、 A が B のインターフェース(BI)に依存するようになりました。なので、 テストをするときに BI を満たしてるクラスにならすり替えても、正常にテストできることが保証されます。
ただし、 B をすり替えるには、 new TestB()
とかコードを書き換えるとかしないといけないので、テストは依然難しいです。
Service Locator を使う
class ServiceLocator {
static services: Map<string, { new (): object }> = new Map();
static register(key: string, service: { new (): object }) {
this.services.set(key, service)
}
static resolve<T extends object>(key: string): T {
return new (this.services.get(key) as { new (): T })
}
}
class A {
exec() {
console.log("exec A");
const b = ServiceLocator.resolve<IB>("B");
b.exec();
}
}
interface IB {
exec: () => void;
}
class B implements IB {
exec() {
console.log("exec B");
}
}
class App {
main() {
ServiceLocator.register("B", B);
const a = new A();
a.exec();
}
}
const app = new App();
app.main();
ServiceLocator というクラスを追加しています。
そして、App で B を ServiceLocator に追加(register)し、 A で ServiceLocator から取り出し(resolve)しています。
依存とか ServiceLocator とかいうから小難しいけど、「それぞれで管理するとややこしいから、一箇所のデータベースで管理しよう」とかに近い気がします。React でいうと Context とか。
これにより、A は BI に依存しているし、実装としても ServiceLocator に依存しているので、テストするときは ServiceLocator に B をモックするクラスを登録するだけで OK になりました。型も実装も直接 B には依存していない状態です。
この実装では ServiceLocator は resolve されるたびにインスタンスを生成( new )してるけど、必ずしもする必要はない。1回インスタンスを生成したらキャッシュする実装にすると、それがたぶんシングルトンパターンになるのでは。わからないけど。。
DI
class A {
constructor(private b: IB) {}
exec() {
console.log("exec A");
this.b.exec();
}
}
interface IB {
exec: () => void;
}
class B implements IB {
exec() {
console.log("exec B");
}
}
class App {
main() {
const b = new B();
const a = new A(b);
a.exec();
}
}
const app = new App();
app.main();
DI コンテナとか考えない普通の DI。やってることは Service Locator よりさらにシンプル。
クラスのインスタンス化時に、引数に依存してるインスタンスを渡してあげる。
Service Locator だと A が Service Locator の存在を意識する必要があったけど、DI だと必要なインスタンスが勝手に引数で入ってくるからそういう意味でもシンプル。
クラスが増えていくと App の中で初期化するのがどんどん難しくなっていくから、それを肩代わりして全部やってくれるのが DI コンテナらしい。
たぶん実装はとても難しいと思うし、あまり理解する必要がない気がするから DI コンテナの実装については考えないでおきます笑
まとめ
何よりインターフェースってやっぱすごいなって思いました。
DI コンテナは実装が難しそうだけど、すでにフレームワークで用意されているなら ServiceLocator より優れていると感じました。