AngularJSの Dependency Injection

AngularJSのチュートリアルをやったりドキュメントをみたりしている。

いわゆるData Binding系のフレームワークで、そのバインドの仕方も興味あるところなんだけど、Testacularで知られるようにテストのしやすさにもずいぶん気を使っているようで、DIの仕組みも興味深い。

というか、チュートリアル進めてたらいきなり
http://docs.angularjs.org/tutorial/step_05

  describe('PhoneListCtrl', function(){
    var scope, ctrl, $httpBackend;

    beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
      $httpBackend = _$httpBackend_;
      $httpBackend.expectGET('phones/phones.json').
          respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);

      scope = $rootScope.$new();
      ctrl = $controller(PhoneListCtrl, {$scope: scope});
    }));

とかいう謎のコードが出てたのでメモ。

injector

さてAngularJSではコントローラを普通、PhoneDetailCtrl($scope)とか書くわけだけど、ここに何かPhoneDetailCtrl($scope, $httpBackend)とか仮引数を追加するとフレームワーク側がコントローラを呼び出すときに$httpBackendというビルトインのサービスを渡してくれるわけだ。

それを実現するためには、フレームワーク側がコントローラの仮引数を知っていなきゃならない。リフレクションAPIなんて無いよなどーなってんのと思ったら、単にFunction.prototype.toString()した結果を正規表現で切り分けていた。この辺をやっているのがinjector.annotate()。

あと、それだと圧縮ツールとかで変数名変えられるとまずいので、DIの書き方は他にも方法があります。

variable name shadowing

で、$httpBackendはわかったけど_$httpBackend_って何だよって話だけど、これは名前の衝突を避けるためのvariable name shadowingというものらしく、要するにアンダースコアで囲んだらそれを無視してサービス名と解釈するというもの。ちなみにこれがはいったコミットがこれ。

でこのときはドキュメントがあったみたいだけど、

ここでそのドキュメントがなくなってしまっている。

名前の衝突を避けたいだけなら、引数の名前を変えずにローカル変数の名前を変えてもよかったんだろうけど、

  describe('PhoneListCtrl', function(){
    var scope, ctrl, httpBackend;

    beforeEach(inject(function($httpBackend, $rootScope, $controller) {
      httpBackend = $httpBackend;

消えちゃったドキュメントを見ると、

  describe('PhoneListCtrl', function(){
    it("...", inject(function($httpBackend) {
      $httpBackend.expectGET(...);
      ...
    });

    it("...", inject(function($httpBackend) {
      $httpBackend.expectGET(...);
      ...
    });
  });

みたいに既に$httpBackendをたくさん使っちゃってる形からリファクタリングするときに便利でしょ、ということか。

angular.mock.inject()

ここでのinject()とは、angular.mock.inject()のことで、これはangular.jsの中ではなくangular.mock.jsの中にある。通常はフレームワークによって注入される依存関係を、単体テスト時はここで注入しているわけだ。

あーあとあれだ、$httpBackendというビルトインのサービスがあるわけだけど、ここでの$httpBackendはそれじゃなくてangular.mock.$httpBackendになっているようだ。

なんか色々疲れたのでこの辺で。あとhttps://github.com/angular/angularjs-batarangがすごいと思った。