JavaScriptでRequireJSっぽい関数のDIフレームワークを実装する


TL;DR; コンポーネントの名前の配列、それ以上の長さの仮引数リストを持つ関数、そしてそれらを引数にとるようなヘルパ関数を使ってDIを実現する。応用すると、コンストラクタの引数へDIしたり、コンポーネントの生成もDIフレームワークで面倒みたり、循環依存の検知したりなど色々できる。

## 前置き

[Dependency injection in JavaScript](http://krasimirtsonev.com/blog/article/Dependency-injection-in-JavaScript) の一部を自分なりに咀嚼して加筆修正したものなので、元記事が読めるなら元記事も読むことをおすすめします。内容自体は元記事そのままではなく、コード中の名前などの自分なりに意味が通るように変更しています。翻訳記事ではありません。

記事中のコードは Node.js v0.12.4 で軽く動作確認しています。

## 背景

[Hipache](https://github.com/hipache/hipache) というOSSをforkして自前のリバースプロキシ[Lopache](https://github.com/crowdworks/lopache)を実装する過程で、どうやって薄いDIフレームワークを実装するかを悩んだので、参考のためRequireJSにおける関数のDIについてまとめた。

あの著名なRequireJSについて今更・・・という感じもするが、RequireJSにおけるDIに特化してまとめられた日本語の文章が見つからなかった書いてみる。

## RequireJSっぽい関数のDI

RequireJSにおける関数のDIでは、関数の引数をDIする。そのために、DIしたいコンポーネントの名前の配列と、DI先の関数（少なくとも前述の配列以上の長さの仮引数リストを持つ）の二つを、依存性の解決をしてくれるヘルパ関数に渡す。

以下は[RequireJS](http://requirejs.org/docs/api.html#define)のような `define` ヘルパ関数を使うコード例（同じではない）。`service` と `router` がDIしたいコンポーネントの名前で、それが関数の引数を通して注入される。

```javascript
define(['service', 'router'], function(service, router) {       
    // ...
});
```

ヘルパ関数の名前は `define` である必要はない。例えば RequireJSっぽいのDIを実現するために、以下のように `injector` オブジェクトの `inject` 関数を使うような実装も考えられる。

```javascript
var doSomething = injector.inject(['service', 'router'], function(service, router) {
    expect(service().name).to.be('Service');
    expect(router().name).to.be('Router');
});
doSomething();
```

以下のコード例では、 `inject` 関数が依存性を解決してくれる `service` と `router` 以外に、`other` を要求するような関数を定義している。`inject` は `other` を解決しない。ではどうするかというと、 `inject` 関数は `service` と `router` だけをbindした新しい関数を返す。コード例では `doSomething` がそれ。元の関数の第1引数と第2引数はbindされているので、第3引数だけが残る。だから、 `doSomething` は元の関数の第3引数だけをとる1引数の関数になる。

```javascript
var doSomething = injector.inject(['service', 'router'], function(service, router, other) {
    expect(service().name).to.be('Service');
    expect(router().name).to.be('Router');
    expect(other).to.be('Other');
});
doSomething("Other");
```

## 実装

前述の「DIしたいコンポーネントの名前の配列」を `deps`、それ以上の長さの仮引数リストを持つDI先の関数を `func` とすると、 `inject` 関数は以下のように実装できる。

```javascript
function(deps, func) {
    var args = [];
    for (var i=0; i<deps.length, d=deps[i]; i++) {
        args.push(this.dependencies[d]);
    }
    return function() {
        func.apply(null, args.concat(Array.prototype.slice.call(arguments, 0)));
    }
}
```

このままだと、「DI先の関数が要求する数のコンポーネントが与えられてない」というようなありがちなプログラミングエラーまたは設定ミスを起こした時、DI先の関数の実引数が `undefined` になってしまい、おなじみの `TypeError: Cannot read property 'foo' of undefined` を起こしてしまう。エラーをわかりやすくしたい、またはfail-fastさせたい場合は、エラー処理をする。

```javascript
function(deps, func) {
    var args = [];
    for (var i=0; i<deps.length, d=deps[i]; i++) {
        if (this.dependencies[d]) {
            args.push(this.dependencies[d]);
        } else {
            throw new Error('Can\'t resolve ' + d);
        }
    }
    return function() {
        func.apply(null, args.concat(Array.prototype.slice.call(arguments, 0)));
    }        
}
```

## 応用: オブジェクトのコンストラクタに対するDI

さらに、このままだとこの関数をオブジェクトのコンストラクタとして使う場合に、bindされた `this` が捨てられてしまうので、捨てられないようにしたい場合は `func.apply(this, ...)` しておけばよい。

```javascript
function(deps, func) {
    var args = [];
    for (var i=0; i<deps.length, d=deps[i]; i++) {
        if (this.dependencies[d]) {
            args.push(this.dependencies[d]);
        } else {
            throw new Error('Can\'t resolve ' + d);
        }
    }
    return function() {
        func.apply(this, args.concat(Array.prototype.slice.call(arguments, 0)));
    }
}
```

これらを踏まえると、最低限の `injector` は以下のように定義できる。

```javascript
var injector = {
    dependencies: {
        service: {name:'service'},
        router: {name:'router'}
    },
    inject: function(deps, func) {
	    var args = [];
       for (var i=0; i<deps.length, d=deps[i]; i++) {
            if (this.dependencies[d]) {
                args.push(this.dependencies[d]);
	        } else {
               throw new Error('Can\'t resolve ' + d);
	        }
       }
	    return function() {
           func.apply(this, args.concat(Array.prototype.slice.call(arguments, 0)));
	    }
   }
};
```

オブジェクトのコンストラクタを定義する場合は以下のように使う。

```javascript
var Foo = injector.inject(['service', 'router'], function(service, router) {
    this.service = service;
    this.router = router;
});

new Foo;
//=> { service: { name: 'service' }, router: { name: 'router' } }
```

さらに、実用的には `injector.dependencies` に入るコンポーネントを定義するヘルパなどをつくることもあるかもしれない。
RequireJS では、以下のように [`define` 関数の第一引数でモジュール名を明示したり](http://requirejs.org/docs/api.html#modulename
)、

```javascript
//Explicitly defines the "foo/title" module:
define("foo/title",
    ["my/cart", "my/inventory"],
    function(cart, inventory) {
        //Define foo/title object in here.
    }
);
```

[ディレクトリとファイルの名前から暗黙的にモジュール名を決めてくれたりする](http://requirejs.org/docs/api.html#defdep)。

## 応用: 依存先コンポーネントの作成もDIフレームワークで

ここまでくるともはや元記事の範囲外だが、serviceやrouter自体の依存性もDIフレームワークで完結させたい場合もあるだろう。
その場合、以下のようにコンポーネントの定義を `definitions`、定義からコンポーネントを生成する関数を `create` として以下のような実装が考えられる。

```javascript
var Service = function() {
    this.name = 'service';
};

var Router = function() {
    this.name = 'router';
};

var injector = {
    dependencies: {},
    definitions: {
       service: [ [], Service ],
       router: [ [], Router ]
    },
    create: function(name) {
        var definition = this.definitions[name];
        if (definition) {
            var ctor = this.inject.apply(this, definition);
	        return new ctor();
	    } else {
            throw new Error('Can\'t instantiate ' + name);
        }
    },
    resolve: function(name) {
        var dep;
        if (this.dependencies[name]) {
            dep = this.dependencies[name];
	    } else {
	        try {
	            dep = this.create(name);
	        } catch(e) {
	            throw new Error('Can\'t resolve ' + name);
	        }
	    }
	    return dep;
    },
    inject: function(deps, func) {
	    var args = [];
       for (var i=0; i<deps.length, d=deps[i]; i++) {
            args.push(this.resolve(d));
       }
	    return function() {
           func.apply(this, args.concat(Array.prototype.slice.call(arguments, 0)));
	    }
   }
};

var Foo = injector.inject(['service', 'router'], function(service, router) {
    this.service = service;
    this.router = router;
});

new Foo;
//=> { service: { name: 'service' }, router: { name: 'router' } }
```

## 応用: 依存性の循環を検知してエラーにする

さらに、このままだ依存関係に循環があって解決しようがない場合に無限ループになってしまうので、循環があるときにはエラーにしたくなる。
今回は、依存関係のグラフにおいて、既に登場した未解決のコンポーネントが次に登場したときにエラーにする…という考え方でこれを実現してみる。

以下のコード例では、意図的にserviceとappの間に依存関係の循環をつくって、それを検知してエラーにする。

```javascript
var Service = function() {
    this.name = 'service';
};

var Router = function() {
    this.name = 'router';
};

var App = function(service) {
    this.name = 'app';
    this.service = service;
};

var injector = {
    dependencies: {},
    definitions: {
       service: [ ['app'], Service ],
       router: [ [], Router ],
       app: [ ['service'], App ]
    },
    create: function(name, context) {
        var context = this.contextWithDefaults(context);
        var definition = this.definitions[name];
        if (definition) {
            var ctor = this.inject.apply(this, definition.concat([context]));
	        return new ctor();
	    } else {
            throw new Error('Can\'t instantiate ' + name);
        }
    },
    resolve: function(name, context) {
        var context = this.contextWithDefaults(context);
        var dep;

        if (context.unresolved[name]) {
            throw new Error('Cyclic dependency from and to ' + name + ' detected');
        } else {
            context.unresolved[name] = true;
        }

        if (this.dependencies[name]) {
            dep = this.dependencies[name];
	    } else {
	        try {
	            dep = this.create(name, context);
	        } catch(e) {
	            throw new Error('Can\'t resolve ' + name + ': ' + e.message);
	        }
	    }
	    return dep;
    },
    inject: function(deps, func, context) {
        var context = this.contextWithDefaults(context);
	    var args = [];
       for (var i=0; i<deps.length, d=deps[i]; i++) {
            args.push(this.resolve(d, context));
       }
	    return function() {
           func.apply(this, args.concat(Array.prototype.slice.call(arguments, 0)));
	    }
   },
   contextWithDefaults: function(context) {
       return context || { unresolved: {} };
   }
};

var Foo = injector.inject(['service', 'router'], function(service, router) {
    this.service = service;
    this.router = router;
});

new Foo;
//=> Error: Can't resolve service: Can't resolve app: Cyclic dependency from and to service detected
```

最後の `Error: Can't resolve service: Can't resolve app: Cyclic dependency from and to service detected` というエラーを左から読むと、「serviceを解決できなかった原因はappを解決できなかったことで、その原因はserviceからserviceへの依存関係の循環が検知されたこと」となる。コード例ではserviceもappも循環しているといえるが、今回はserviceの循環が検知された。これは、今回は `Foo` の定義によってappより先にserviceを解決しようとするので、依存関係グラフにおいてまずserviceが登場するから。

## コンポーネントの定義もヘルパ関数で

再利用性や保守性を考えると、 `injector.definitions` を `injector` にハードコーディングするのは避けたくなる。

```
var injector = {
    definitions: {
       service: [ ['app'], Service ],
       router: [ [], Router ],
       app: [ ['service'], App ]
    },
```

ハードコーディングを避けるためには、コンポーネントを定義するヘルパ関数をつくるという方法が考えられる。
例えば、その関数を `define` という名前にしたとすると、

```
injector.define('service', [], Service);
injector.define('router', [], Router);
```

このように書けないだろうか。

というわけで、以下にその実装例を示す。

```
var Service = function() {
    this.name = 'service';
};

var Router = function() {
    this.name = 'router';
};

var injector = {
    dependencies: {},
    definitions: {},
    define: function(name, deps, func) {
        this.definitions[name] = [deps, func];
    },
    create: function(name, context) {
        var context = this.contextWithDefaults(context);
        var definition = this.definitions[name];
        if (definition) {
            var ctor = this.inject.apply(this, definition.concat([context]));
	        return new ctor();
	    } else {
            throw new Error('Can\'t instantiate ' + name);
        }
    },
    resolve: function(name, context) {
        var context = this.contextWithDefaults(context);
        var dep;

        if (context.unresolved[name]) {
            throw new Error('Cyclic dependency from and to ' + name + ' detected');
        } else {
            context.unresolved[name] = true;
        }

        if (this.dependencies[name]) {
            dep = this.dependencies[name];
	    } else {
	        try {
	            dep = this.create(name, context);
	        } catch(e) {
	            throw new Error('Can\'t resolve ' + name + ': ' + e.message);
	        }
	    }
	    return dep;
    },
    inject: function(deps, func, context) {
        var context = this.contextWithDefaults(context);
	    var args = [];
       for (var i=0; i<deps.length, d=deps[i]; i++) {
            args.push(this.resolve(d, context));
       }
	    return function() {
           func.apply(this, args.concat(Array.prototype.slice.call(arguments, 0)));
	    }
   },
   contextWithDefaults: function(context) {
       return context || { unresolved: {} };
   }
};

injector.define('service', [], Service);
injector.define('router', [], Router);

var Foo = injector.inject(['service', 'router'], function(service, router) {
    this.service = service;
    this.router = router;
});

new Foo;
//=> { service: { name: 'service' }, router: { name: 'router' } }
```

## まとめ

まとめると、

(1) コンポーネントの名前のリスト
(2) (1)以上の長さの仮引数リストを持つ関数

があって、(1)と(2)をとるようなヘルパ関数を使って、RequireJSっぽい関数のDIフレームワークを実装できる。

応用すると、コンストラクタの引数へDIしたり、コンポーネントの生成もDIフレームワークで面倒みたり、循環依存の検知したりなど色々できる。

## もっと詳しく知りたい方へ

さまざまなDIの実装方法まで踏み込んだ解説は [Dependency injection in JavaScript](http://krasimirtsonev.com/blog/article/Dependency-injection-in-JavaScript) に詳しく書かれているので、それを読もう。