読者です 読者をやめる 読者になる 読者になる

angularでDIされるserviceがsingletonで困る場合にはfactoryを返すことを考えてみる

js angular

はじめに

angularでは各serviceの利用時に指定した引数の箇所にDIされる。 これらで生成されるserviceは基本的にはsingleton objectになっている。 このためDIされた各service中では同一のオブジェクトを共有している。

例えば、以下の様なコードがあった場合には、以下の様な挙動を示す。

counter.js

'use strict';

function counter($timeout){
  var i = 0;
  return (function(){
    i += 1;
    return i;
  });
}

module.exports = {
  counter: counter
};

main.js (setupはnode.js上でangularを動かすための特別な関数)

'use strict';

require('./setup')(function(angular){
  var c = require('./counter');

  angular
    .module("app", [])
    .factory("counter", ["$timeout", c.counter])
    .service("useCounter1", function(counter){return {use: counter};})
    .service("useCounter2", function(counter){return {use: counter};})
  ;

  var inj = angular.injector(["ng", "app"]);
  var c1 = inj.get("useCounter1");
  var c2 = inj.get("useCounter2");

  console.log(c1.use());
  console.log(c2.use());
  console.log(c1.use());
});

実行結果

1
2
3

useCounter1,とuseCounter2でDIされるcounterは同一のもののため状態を共有している。 複数の状態を持つserviceを作りたい場合にはどうすれば良いかというのが今回の話。

複数の状態を持つservice

複数の状態を持つserviceを作る方法として以下の3種類が思いつく

  • service以外に状態を持たせる
  • angularのDI上に乗せずに自前で初期化する
  • DIに登録するのはfactory objectにする

それぞれ例をあげて見ていくことにする

service以外に状態を持たせる

service以外に状態を持たせると言うのは状態を保持する場所を引数として取るということになる。 DIでcontrollerやserviceを紐つけた場合には、そのserviceと密に結びついてしまうので直接DIを用いて状態を保持するオブジェクトを渡す事はできない。 その代わりに Function.prototype.bind() を使ってthisあるいはstateオブジェクトを渡して部分適用した関数を親となるserviceに束縛してみる。

counter2.js

'use strict';

function counter2($timeout){
  return (function(){
    this.count += 1;
    return this.count;
  })
}

module.exports = {
  counter2: counter2
}

main2.js

require('./setup')(function(angular){
  var c = require('./counter2');

  angular
    .module("app", [])
    .factory("counter", ["$timeout", c.counter2])
    .factory("useCounter1", function(counter){
      var state = {count: 0};
      return {
        use: counter.bind(state)
      };
    })
    .factory("useCounter2", function(counter){
      this.count = 0;
      return {
        use: counter.bind(this)
      };
    })
  ;

  var inj = angular.injector(["ng", "app"]);
  var c1 = inj.get("useCounter1");
  var c2 = inj.get("useCounter2");

  console.log(c1.use());
  console.log(c2.use());
  console.log(c1.use());
});

一応このコードは想定する仕様を満たす。

実行結果

1
1
2

ただし、serviceが必要とする親serviceに対して特定のinterfaceを満たすことを求めることになるのであまり綺麗ではないように感じる。 (typescriptなど型を指定しておけるようになると幾分かマシになるかもしれない。) その上同一の親serviceが複数の状態を持ちたい場合(例えばAServiceがxCounter,yCounterを同時に保持したい場合など)に少し工夫が必要となる。

また、状態を保持するオブジェクトをbindしておくというのは、他の言語のプログラマからすると少し奇妙なものに写る可能性がある。

angularのDI上に乗せずに自前で初期化する

あくまで作ったサービスは1つのオブジェクトとして自身の内に状態を閉じ込めておきたいと思うことがあるかもしれない。 それならばいっそ全てを自前で初期化して見ることにする。

counter3.js

'use strict';

function counter3($timeout){
  var i = 0;
  return (function(){
    // do something with $timeout or other services
    i += 1;
    return i;
  });
}

module.exports = {
  counter3: counter3
};

main3.js

'use strict';

require('./setup')(function(angular){
  var c = require('./counter3');

  angular
    .module("app", [])
    .factory("useCounter1", function($timeout){
      return {
        use: c.counter3($timeout)
      };
    })
    .factory("useCounter2", function($timeout){
      return {
        use: c.counter3($timeout)
      };
    })
  ;

  var inj = angular.injector(["ng", "app"]);
  var c1 = inj.get("useCounter1");
  var c2 = inj.get("useCounter2");

  console.log(c1.use());
  console.log(c2.use());
  console.log(c1.use());
});

こちらでも一応期待した仕様通りの動作を行う。しかしちょっと考えるとこれではマズイということが分かる。 angularのDIに乗らずに自前で初期化することを選んだ場合、作ったservice(この例ではcounter3)を利用したいservice及びcontroller中では、作ったservice自体が依存するserviceの全てをDIして取り出さなくてはいけなくなっている。

例えば今回作ったcounter3は$timeoutだけに依存しているがこれが増えた場合や他のserviceも同時に利用したいと思った場合にはDIする数が爆発してしまう。例えば以下のように。

app.controller("myCtrl", ["a","b","c","d","e","f","g","h",,,,"$timeout", function(....){
]);

もちろん極端な例だがなるべくangularのDIによる依存componentの取得は利用した上で状態を共有しないserviceを作成したい。

DIに登録するのはfactory objectにする

以下の2つを満たす物を作りたいということだった。

  • 通常のDIによるsingleton objectではなく同一serviceに対して複数のobjectを利用したい
  • DIによる依存componentの受け渡しの利点は享受したままにしたい

実はこれは何も難しいことはなく単にfactory objectあるいはfactory関数を定義すれば済む話だった。 その上元のcounterの定義をそのまま利用できる。

以下のような形になる。

counter.js(再掲)

'use strict';

function counter($timeout){
  var i = 0;
  return (function(){
    i += 1;
    return i;
  });
}

module.exports = {
  counter: counter
};

main4.js

'use strict';

require('./setup')(function(angular){
  var c = require('./counter');

  function counterFactory($timeout){
    return function(){
      return c.counter($timeout);
    };
  }

  angular
    .module("app", [])
    .factory("counter", ["$timeout", counterFactory])
    .service("useCounter1", function(counter){return {use: counter()};})
    .service("useCounter2", function(counter){return {use: counter()};})
  ;

  var inj = angular.injector(["ng", "app"]);
  var c1 = inj.get("useCounter1");
  var c2 = inj.get("useCounter2");

  console.log(c1.use());
  console.log(c2.use());
  console.log(c1.use());
});

ここで言うfactory関数とはcounterFactoryのこと。

もちろん複数の状態を持つことができている

実行結果

1
1
2

gist

手元で実行したい場合はこちら

https://gist.github.com/podhmo/1efb5e7d063f63d04039