TypeScript
DI
DependencyInjection

何故DIコンテナが必要なのか

「何故DIコンテナが必要なのか」について、自分なりに説明を試みる。

目次

  1. 何故DI(依存性注入)が必要なのか
  2. DIでは一体誰がnewするべきか
  3. newするためのクラスを別に作る
  4. newをDIコンテナに任せる
  5. Q&A

1. 何故DI(依存性注入)が必要なのか

まず、DI(依存性注入)が必要なケースについて説明する。

下記のように、ハードウェア制御クラスGreatHardwareがあり、クラスLowLayerはそれを利用しているとする。クラスGreatHardwareは、実際のハードウェアがないと動かないとする。

この構成には問題がある。クラスLowLayerが、実際のハードウェアがないと動かないクラスGreatHardwareを直接newしてしまっているため、実際のハードウェアがないとテストできない。

// 構成例1
class GreatHardware {
    somethingGreat(){
        // 何かハードウェア依存の処理を行う
    }
}

class LowLayer {
    hardware:GreatHardware;
    constructor(){
        this.hardware = new GreatHardware();
    }

    doHoge() {
        this.hardware.somethingGreat();
    }
}

これを解決するには、下記の構成例2のように「DI(依存性注入)」を使うことができる。ここでは、LowLayerのコンストラクタからGreatHardwareのインスタンスを注入している。これによりインターフェースIGreatHardwareを実装したモックをLowLayerのコンストラクタに注入すれば実際のハードウェアがなくてもLowLayerのテストができるということになる。

// 構成例2
interface IGreatHardware {
    somethingGreat();
}

class GreatHardwareImpl implements IGreatHardware {
    somethingGreat(){
        // 何かハードウェア依存の処理を行う
    }
}

class LowLayer {
    hardware:IGreatHardware;
    constructor(hardware:IGreatHardware){
        this.hardware = hardware;
    }

    doHoge() {
        this.hardware.somethingGreat();
    }
}

2. DIでは一体誰がnewするべきか

しかし、めでたしめでたしとはいかない。構成例2を実際に動かすには、誰かがGreatHardwarenewしてLowLayerに渡さなくてはならない。

例えば構成例3が考えられるだろう。しかし、これでは構成例1と同じ問題が起こる。結局MiddleLayerGreatHardwarenewしなければならず、MiddleLayerGreatHardwareに依存してしまうからだ。

// 構成例3
interface IGreatHardware {
    somethingGreat();
}

class GreatHardwareImpl implements IGreatHardware {
    somethingGreat(){
        // 何かハードウェア依存の処理を行う
    }
}

class LowLayer {
    hardware:IGreatHardware;
    constructor(hardware:IGreatHardware){
        this.hardware = hardware;
    }

    doHoge() {
        this.hardware.somethingGreat();
    }
}

class MiddleLayer {
    lowLayer:LowLayer;
    constructor(){
        this.lowLayer = new LowLayer(new GreatHardware());// <- 結局newしている
    }

    doFuga(){
        this.lowLayer.doHoge();
    }
}

ではさらに上のレイヤーにGreatHardwarenewする責任を持たせるようにしたらどうだろうか(構成例4)。

しかし、結局これだとUIレイヤーが具体的なハードウェアがないと動かないクラスに依存することになってしまった。

構成例1よりもまずい状況なのではないだろうか?

// 構成例4
interface IGreatHardware {
    somethingGreat();
}

class GreatHardwareImpl implements IGreatHardware {
    somethingGreat(){
        // 何かハードウェア依存の処理を行う
    }
}

interface ILowLayer {
    doHoge();
}

class LowLayerImpl implements ILowLayer {
    hardware:IGreatHardware;
    constructor(hardware:IGreatHardware){
        this.hardware = hardware;
    }

    doHoge() {
        this.hardware.somethingGreat();
    }
}

interface IMiddleLayer {
    doFuga();
}

class MiddleLayerImpl implements IMiddleLayer {
    lowLayer:ILowLayer;
    constructor(lowLayer:ILowLayer){
        this.lowLayer = lowLayer;
    }

    doFuga(){
        this.lowLayer.doHoge();
    }
}

interface IHighLayer {
    doFoo();
}

class HighLayerImpl implements IHighLayer {
    middleLayer:IMiddleLayer;
    constructor(middleLayer:IMiddleLayer){
        this.middleLayer = middleLayer;
    }

    doFoo(){
        this.middleLayer.doHoge();
    }
}

class UIController {
    highLayer:IHighLayer;
    constructor(){
        this.highLayer = new HighLayerImpl(new MiddleLayerImpl(new LowLayerImpl(new GreatHardwareImpl())));// <- UIが実際のハードウェアがないと動かないクラスに依存している。
        this.highLayer.doFoo();
    }
}

3. newするためのクラスを別に作る

構成例4を見たとき、

new HighLayerImpl(new MiddleLayerImpl(new LowLayerImpl(new GreatHardwareImpl())));

この部分をさらに誰か別のクラスの責任にしたらいいのでは?と思う人がいると思う。

それをやって見たのが構成例5である(UIControllerより下のレイヤはこれまでの構成例と同じなので省略)。

// 構成例5
class UIController {
    highLayer:IHighLayer;
    constructor(){
        this.highLayer = GreatFactory.createHithLayer();
        this.highLayer.doFoo();
    }
}

class GreatFactory {
    static createHighLayer(){
        return new HighLayerImpl(new MiddleLayerImpl(new LowLayerImpl(new GreatHardwareImpl())));
    }
}

さらに一貫性の観点から、UIControllerインスタンスの生成もGreatFactoryが任されると考えてみよう。構成例6である。

// 構成例6
class UIController {
    highLayer:IHighLayer;
    constructor(highLayer:IHighLayer){
        this.highLayer = highLayer;
        this.highLayer.doFoo();
    }
}

class GreatFactory {
    static createUiController(){
        return new UIController(new HighLayerImpl(new MiddleLayerImpl(new LowLayerImpl(new GreatHardwareImpl()))));
    }
}

これによって全てのレイヤーが分離し、相互依存がない状態になった。GreatFactory以外のクラスの依存性は外部から注入でき、モックテストができる。

ただ、最後の問題はGreatFactoryだ。ここのメンテナンス性には問題がある。例えば、MiddleLayerImplのコンストラクタ引数が増加したらどうだろうか。メンテナンスが大変ではないだろうか。

4. newをDIコンテナに任せる

これをDIコンテナを使って書き直すとすごく簡単になる。

ここでは簡単のため、架空のDIコンテナを用いる。

この架空のDIコンテナは、コンストラクタの前に@Inject()を記載すると、別途書いた設定通りに引数の型に対応するインスタンスを生成してセットしてくれる。TypeScriptのinterfaceは実行時には認識できないが、何かの魔法で認識されるものとする。@InjectはDIコンテナが動いていない場合は(例えばテストの時など)、何も書いていないのと同じになる。

このDIコンテナの設定ファイルは下記のように書く。=の左辺の型には、右辺のクラスのインスタンスが渡される。

IHighLayer=HighLayerImpl
IMiddleLayer=MiddleLayerImpl
ILowLayer=LowLayerImpl
IGreatHardware=GreatHardwareImpl

構成例7にこのDIコンテナを使った例を示す。

// 構成例7
interface IGreatHardware {
    somethingGreat();
}

class GreatHardwareImpl implements IGreatHardware {
    somethingGreat(){
        // 何かハードウェア依存の処理を行う
    }
}

interface ILowLayer {
    doHoge();
}

class LowLayerImpl implements ILowLayer {
    hardware:IGreatHardware;

    @Inject()
    constructor(hardware:IGreatHardware){
        this.hardware = hardware;
    }

    doHoge() {
        this.hardware.somethingGreat();
    }
}

interface IMiddleLayer {
    doFuga();
}

class MiddleLayerImpl implements IMiddleLayer {
    lowLayer:ILowLayer;

    @Inject()
    constructor(lowLayer:ILowLayer){
        this.lowLayer = lowLayer;
    }

    doFuga(){
        this.lowLayer.doHoge();
    }
}

interface IHighLayer {
    doFoo();
}

class HighLayerImpl implements IHighLayer {
    middleLayer:IMiddleLayer;

    @Inject()
    constructor(middleLayer:IMiddleLayer){
        this.middleLayer = middleLayer;
    }

    doFoo(){
        this.middleLayer.doHoge();
    }
}

class UIController {
    highLayer:IHighLayer;

    @Inject()
    constructor(highLayer:IHighLayer){
        this.highLayer = highLayer;
        this.highLayer.doFoo();
    }
}

これにより設定通りにインスタンスを生成してくれる。

メンテナンス性も向上している。下記のようにMiddleLayerImplのコンストラクタの引数が増えたときも、

class MiddleLayerImpl implements IMiddleLayer {
    lowLayer:ILowLayer;

    @Inject()
    constructor(lowLayer:ILowLayer, otherLayer:IOtherLayer){ // <- otherLayerが増えた
        this.lowLayer = lowLayer;
    }

    doFuga(){
        this.lowLayer.doHoge();
    }
}

下記のようにDIコンテナの設定に追記すれば正しくインスタンスを生成してセットしてくれる。

IHighLayer=HighLayerImpl
IMiddleLayer=MiddleLayerImpl
ILowLayer=LowLayerImpl
IGreatHardware=GreatHardwareImpl
IOtherLayer=OtherLayerImpl // <- otherLayerの設定を追加

5. Q&A

  • Q1. 大げさすぎるように思える。なぜ本番のシステムにDIコンテナという複雑な仕組みを持ち込む必要があるのだろうか。むしろ、テストの時だけ使う仕組みを用意して、GreatHardwareを差し替えればそれで済むのではないだろうか。
  • A. 「テストの時だけに使う仕組み」はむしろ危険なことが多い。テストの時だけにしか使わないから、もしバグがあったときに非常に見つかりにくい。テストの時はできるだけ仕組みを使わないべきだと思う。その点、DIコンテナを使った構成であれば、テストのときはクラスはただのクラスであり、単純にテストすれば良いので非常にシンプルになる。