Modules should be fast and side-effect free と Guice は言っているがPlay Frameworkはそうなってはいない話

github.com

Modules should be fast and side-effect free

Modules should be fast and side-effect free(モジュールは速くて副作用なしであるべき)と GuiceWiki にはあります。

これは Guice モジュールの中で DB に接続したりスレッド開始したりするなという話で、これに従うのならば Guice の Module (extends AbstractModule したクラスの中とか)の他にも Module を構成する各クラスのコンストラクタの中でも副作用を起こしてはいけないことになります。

副作用を起こしてはいけない理由としては

  • Modules start up, but they don't shut down.
  • Modules should be tested.
  • Modules can be overridden.

Guice は挙げています。

Guice の意図に沿ったコードを書くと初期化処理、終了処理のためのメソッドをコンストラクタ以外に用意して、アプリケーションの起動、終了時になんらかの形で呼んであげることになります。

public interface Service {
  void start() throws Exception;
  void stop();
}
 public static void main(String[] args) throws Exception {
    Injector injector = Guice.createInjector(
        new WebserverModule(),
        ...
    );

    Service webserver = injector.getInstance(
        Key.get(Service.class, WebserverService.class));
    webserver.start();
    addShutdownHook(webserver);
  }

サンプルでは単純ですが徹底するとなると例えば設定ファイルを読み込むとかも禁止となるのでなかなか厳しい制限にも感じます。(実際には設定ファイル読み込みとかは OK にして、スレッド開始とかは NG にする、くらいが落とし所かも?)

Play Framework の場合

Play Framework は Guice を使っていますが、Guice のこの主張を完全に無視して副作用をバリバリにつかっています。Play を構成するクラスは大体コンストラクタでスレッド開始したり設定読み込んだりとやって、終了処理はコンストラクタ内で ApplicationLifecycle に登録します。

@Singleton
class NanrakanoService @Inject()(lifecycle: ApplicationLifecycle) {
  val resource = new NanrakanoBackgroundResource().start()
  lifecycle.addStopHook(() => Future.successful(resource.shutdown()))
}

Guicewiki を見たあとでこれを見るとギョッとするんですが実際のところそんなに困ることはないです。

とはいえやっぱり時々は困るし、Guice の推奨しない方法で Guice をヘビーに使っているというのは精神衛生上あまり良くはありません。そして実はコンストラクタで副作用を起こすようになったのは Play が Guice を使い始めた Play 2.4 からの話で、Guice を使っていなかった Play 2.3 以前の Plugin の仕組みでは副作用のためのメソッドが分かれていたのです。

package plugins

import play.api.{Plugin, Application}

class MyPlugin(app: Application) extends Plugin {
  val myComponent = new MyComponent()

  override def onStart() = {
    myComponent.start()
  }

  override def onStop() = {
    myComponent.stop()
  }

  override def enabled = true
}

Guice を使う前は副作用が分かれていたのに、互換性を壊してまで Guice を使い始めたら副作用起こすようになったというのがなんかチグハグだなあって思います。