AngularJSのfactoryとserviceを読み解く(前編)
AngularJSのfactory
とservice
がどうにも覚えられないのでまとめてみた。
まとめ
- factoryはobjectをキャッシュしておく
- serviceはインスタンス化してキャッシュしておく
- providerの謎が深まった
factory
providerにfunctionらしきものを渡している。
$get
はInjectorに指定されたときに一度だけ呼ばれる。
function factory(name, factoryFn) { return provider(name, { $get: factoryFn }); }
https://github.com/angular/angular.js/blob/master/src/auto/injector.js#L666
ここまでだとまだ実行されない。
var app = angular.module('app', ['ngResource']).factory('User', function ($resource) { return $resource('/api/user/:id', {id: "@id"}, {}); });
ここで実行され、以降の呼び出しはreturn $resource('/api/user/:id', {id: "@id"}, {});
の評価結果がキャッシュされたヤツが呼び出される。
app.controller("UserCtrl", function (User) { $scope.user = User.$get(id: 1); });
service
serviceに指定された無名関数はconstructor
という名で$injector.instantiate
の引数に渡され、その処理をfactoryへと渡している。
function service(name, constructor) { return factory(name, ['$injector', function($injector) { return $injector.instantiate(constructor); }]); } function value(name, val) { return factory(name, valueFn(val)); } function constant(name, value) { assertNotHasOwnProperty(name, 'constant'); providerCache[name] = value; instanceCache[name] = value; }
https://github.com/angular/angular.js/blob/master/src/auto/injector.js#L668-L680
$injector.instantiate
は与えられた引数を、あたらにConstructor
クラスが作られ、serviceに登録した関数のprototypeを継承させいる。
なぜArrayの場合は末尾を取り出しているのかはよくわかってない。
['$rootScope', function () { ... }]
的なアレの為に、Arrayの判定してる。
Constructor
クラスをインスタンス化し、invoke(省略)に渡す。
その返り値がObjectまたはFunctionであればそのままをfactoryにわたすが、どちらでもない場合はConstructor
のインスタンスをfactoryに渡している。
function instantiate(Type, locals, serviceName) { var Constructor = function() {}, instance, returnedValue; // Check if Type is annotated and use just the given function at n-1 as parameter // e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]); Constructor.prototype = (isArray(Type) ? Type[Type.length - 1] : Type).prototype; instance = new Constructor(); returnedValue = invoke(Type, instance, locals, serviceName); return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance; }
https://github.com/angular/angular.js/blob/master/src/auto/injector.js#L804-L815
なお、invokeでは(重要なところだけを挙げれば)Constructor
クラスのインスタンスをコンテキストとしてserviceで指定した関数を実行しその結果を返しているだけである。
function invoke(fn, self, locals, serviceName){ if (typeof locals === 'string') { serviceName = locals; locals = null; } var args = [], $inject = annotate(fn, strictDi, serviceName), length, i, key; for(i = 0, length = $inject.length; i < length; i++) { key = $inject[i]; if (typeof key !== 'string') { throw $injectorMinErr('itkn', 'Incorrect injection token! Expected service name as string, got {0}', key); } args.push( locals && locals.hasOwnProperty(key) ? locals[key] : getService(key) ); } if (!fn.$inject) { // this means that we must be an array. fn = fn[length]; } // http://jsperf.com/angularjs-invoke-apply-vs-switch // #5388 return fn.apply(self, args); }
https://github.com/angular/angular.js/blob/master/src/auto/injector.js#L801
ここまで追ってみたけどよくわからないので、続きは明日。
Constructor
で空のクラスをprototypeだけ継承し、インスタンス化する。そののちにinvokによってDependenciesを引数としてserviceで指定したfunctionに渡すことで、DIとしての動きを作っているっぽい。
どう使うのかはまだ曖昧だけど、輪郭が見えてきた。
AngularJSのfactoryとserviceを読み解く(後編)
前編: ngularJSのfactoryとserviceを読み解く(前編)
前回のAngulaJS!(なんちゃらライブ的な) factoryやserviceといったAngularJSの機能をドキュメントを読んだりぐーぐる先生に聞いたりしてもまったくさっぱりよくわからなかったのでAngularJSのコードを追ってみた。
factoryは初期化処理をInjectorで一度だけ実行してその結果をAngularJSのcacheFactoryを使ってキャッシュしているところまではわかった。
しかし、serviceはよくわからなかった。 よくわからないので振り返りから。
.service(name, fn)
- serviceが呼ばれる
- $injector.instantiateにfnが渡される
- $injector.instantiateで別のクラス(仮にAとする)を作成してprototypeを継承する
- $injector.instantiateでクラスAのインスタンスを生成して$injector.invokeにserviceに指定したfnと共に渡す
- $injector.invokeでDIに指定したDependenciesを解析、取得
- $injector.invokeで、引数をdependenciesに、コンテキストをクラスAのインスタンスにしていしてfnを実行する
- $injector.invokeの返り値がobjectまたはfunctionであればそれをfactoryに渡す
- $injector.invokeの返り値がobjectでもfunctionでもなければクラスAのインスタンスをfactoryに渡す
つまり、こういうことだ!
// serviceに登録するfunction(コンストラクタ) var User = function (options) { this.name = options.name || null; this.age = options.age || null; this.set = function (column, value) { this[column] = value; }; this.get = function (column) { return this[column]; }; }; // $injector.instantiate var Constructor= function () {}; Constructor.prototype = User.prototype; var instance = new Constructor(); // $injector.invoke User.apply(instance, [{name: "tarou", age: 10}]); var user = instance;
・・・実にわかりにくい。
気をつけなければならないのは、serviceの第二引数は初期化コールバック関数ではなく、クラスのコンストラクタであること。
factoryとおんなじ用に使うのは不適切で、angularとは別に定義したクラスを指定する(app.service("name", ["$rootScope", MyServiceClass])
のように)か、コンストラクタで完結するクラスを定義すべきだ。
var User = function (options) { this.name = options.name || null; this.age = options.age || null; this.set = function (column, value) { this[column] = value; }; this.get = function (column) { return this[column]; }; }; app.service('user', [User]);
app.service('user', [function () { this.name = options.name || null; this.age = options.age || null; this.set = function (column, value) { this[column] = value; }; this.get = function (column) { return this[column]; }; }]);
serviceはどういう時に使うべき?
factoryとの違いとして、serviceはインスタンスの生成を保証し、かつ同一の名前空間で他のインスタンスは作られない。
デザインパターンで言うところのSingletonであり、アプリケーション内で整合性を管理する用途にもちいられるのだと想定できる。
たとえば、複数のController間で一つのリソースを管理したい場合などがそれに該当すると思われる。
下手に$rootScopeに値を入れたりするよりは固く実装できるのではないだろうか。
まとめ
serviceはSingletonなので、それなりの扱いを用意しよう。 serviceメソッドの第二引数はコンストラクタなのでそれを念頭に置いて利用しよう