AngularJSのDIを使う#AngularJS入門その4

2014.05.15

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

AngularJSでDIを実現する

DI(依存性注入)は、簡単にいえば「オブジェクトに必要な情報を外部設定する手法」です。
Javaの経験者であれば馴染みのある言葉ではないでしょうか。
AngularJSでは、コードが依存性を保持するいくつかの方法を持っています。

オブジェクト/関数が、依存性を取得するには次の方法があります。

  • new演算子で依存性を作成
  • グローバル変数を参照して依存性を作成
  • 依存性が必要な場所に任意で渡す

3つ目の手法を使用すればコンポーネント同士の結合も疎結合となるため、いちばんよい手法です。
この手法であれば、依存性がそのままコンポーネントに引き渡されます。
簡単な例を見てみましょう。

function MyClass(greeterObj) {
  this.greeterObj = greeterObj;
}

MyClass.prototype.say = function(name) {
  this.greeterObj.greet(name);
}

上記の例では、MyClassはgreeterObjと依存関係はありません。greeterObj実行時に渡されるだけになってます。
AngularJSは依存性管理のために「インジェクター(injector)」というものを持っています。
インジェクターとは、依存性のチェックや生成を行う仕組みです。

次のサンプルがインジェクターを使用した例です。

angular.module('myModule', []).
  // インジェクターにどのようにgreeterObjを構築するか
  // greeterObjは、$windowに依存
  factory('greeterObj', function($window) {
    // ファクトリー関数はgreetObjの作成を行う
    return {
      greet: function(message) {
        $window.alert(message);
      }
    };
  });
  
//モジュールから、新しいインジェクター作成.通常はAngularJSが実行する
var injector = angular.injector(['myModule', 'ng']);
// インジェクターから、任意の依存性を要求する
var greeterObj = injector.get('greeterObj');

DIを使用すれば、ハードコーディングする必要はなくなります。
次の例のように依存関係を定義し、インジェクターに依存性の探索を実行させます。

<!-- HTMLコード -->
<div ng-controller="MyController">
  <button ng-click="say()">Hello</button>
</div>
・
・
//コントローラー
function MyController($scope, greeterObj) {
  $scope.say = function() {
    greeterObj.greet('Hello');
  };
}
・
・
//ng-controllerディレクティブは、これを内部で行う
injector.instantiate(MyController);

ng-controllerで指定されたコントローラは、インジェクターとは直接関係していません。
しかし、MyControllerの依存性はちゃんと満たしています。
コード上はインジェクターを扱ってはいませんが、必要な依存性はちゃんと設定されているのがわかります。

依存性設定、いくつかの方法

DIを実現したいとき、インジェクターが依存関係を解決するためにアノテーションを記述する必要があります。
インジェクターは、どのサービスをDIするか事前に知っている必要があります。
ここではそのいくつかの方法を解説します。

・依存関係の推察
依存関係を設定する一番単純な方法です。関数のパラメータ名を依存関係の名前とみなして設定します。

function MyController($scope, greeterObj) {
  ...
}

インジェクターは、関数の宣言を調べてパラメータ名を抽出してDIするサービス名を調査します。
$scopeとgreeterObjは、DIされる2つのサービスです。
上の方法が最も単純なDI方法ですが、パラメーター名をみてDIしているので、
それを変更される処理(たとえばJavaScriptの圧縮/難読化など)を行うと動作しなくなってしまいます。
この方法はデモやモック用途のアプリでのみ使用するようにしたほうがいいでしょう。

・$injectアノテーション
圧縮や難読化による関数パラメータ名の変更をしてもサービスが正しくDIされるようにするため、
$injectプロパティを使ったアノテーションを行うことができます。
$injectプロパティは、DIするサービス名の配列となっています。

var MyController = function(new$scope, newGreeterObj) {
  ...
}

MyController.$inject = ['$scope', 'greeterObj'];

$inject配列の値の順序はDIする引数の順序と一致する必要があります。
上記の例では、$scopeはnew$scopeに、greeterObjは、 newGreeterObjにそれぞれDIされます。

・インラインアノテーション
$injectアノテーション形式は、DIする際に下記のように冗長になることがあります。

someModule.factory('greeterObj', function($window) {
  ...
});
・
・
var greeterFactory = function(new$window) {
  ...
};
greeterFactory.$inject = ['$window'];
someModule.factory('greeterObj', greeterFactory);

微妙に面倒ですね。このような場合、次のように少しシンプルに記述することもできます。

someModule.factory('greeterObj', ['$window', function(new$window) {
  ...
}]);

これらのDI設定方法は同じように動作し、DIがサポートされるどこでも使用可能です。

DIはどこで使用できるか

DIは、コントローラとファクトリー用関数でよく使用されます。

・コントローラでDI
コントローラはアプリの振る舞いを定義します。 コントローラ宣言では、配列を使用したDI方法がよく使用されます。

someModule.controller('MyController', 
['$scope', 'service1', 'service2', function($scope, service1, service2) {
  ...
  $scope.someMethod = function() {
    ...
  }
  ...
}]);

このようにすることで、コントローラがグローパル関数化される事を防ぎ、JavaScript圧縮化への対処にもなります。

・ファクトリーメソッドでDI
ファクトリーメソッドは、AngularJS内のさまざまなオブジェクト作成を行います。
(ディレクティブ/サービス/フィルター等)
ファクトリーメソッドはモジュールに登録されます。なお、次のようなファクトリー宣言手法が推奨されてます。

angular.module('myModule', []).
  config(['depProvider', function(depProvider){
    ...
  }]).
  factory('serviceId', ['depService', function(depService) {
    ...
  }]).
  directive('directiveName', ['depService', function(depService) {
    ...
  }]).
  filter('filterName', ['depService', function(depService) {
    ...
  }]).
  run(['depService', function(depService) {
    ...
  }]);

まとめ

今回はAngularJSを代表する機能の1つ、DIについて解説しました。
次回はAngularJSのサービスについて解説する予定です。

参考サイトなど