オブジェクトの名づけ親

特にJavaだとそうなのだと思うのだが、ひたすらクラスを作ることになる。自分はこれを特にデメリットだと思っていないし、逆にクラスごとに責任範囲を考えさせられるので良いことだと思っている。(それを考えるのが面倒くさいという人は、なぜプログラマをしているのだろうか……)しかし、大量に名前を考えなくてはいけなくなるので、名前付けの引き出しを持っておかないと苦しいことになる。

そういった引き出しを作るために、以前Classname Cheatsheetというページを作った。これはクラス名でよくある名前、特にSuffixをオブジェクトのカテゴリごとに分類したものだ。まだ未完成なのだが、複数のClassnamerっぽいサイトから引っ張ってきたので、そこそこの量はあると思う。

このチートシートをつかっても、微小な差異はわからない。特にObject Constructionのところは微妙なところがあっていつも迷う。なので、いくつかについて自分が考えていることをまとめてみた。

Builder

Fluent interfaceになっているsetterをもつオブジェクト。

new Builder()
    .setA(new A())
    .setB(new B())
    .build();

基本的にValue objectみたいなImmutableなものを作るときに使うように思える。コンストラクタの部分適用っぽい感じもする。幾つか注意したい点がある。

  • 必須なパラメーターはsetterを提供するのではなく、コンストラクタかbuildメソッドにもたせると良い。
  • Builderはgetterを提供するべきではないように思える。というのも、getterを提供するとStateみたいに使われてしまって、Builder本来の役割から外れた使い方ができてしまうからだ。
  • BuilderはDIによってInjectされるべきではないように思える。Moduleの構成によっては、Builderが複数のオブジェクトによって共有されるので、ここでDIを使うとそういう可能性がある印象を与えるように感じられる。

    • この印象はObjectGraph.get()をコード中で呼び出すことによって、またはProvider.get()を使うことによって緩和されてしまう。というのも、懸念点は一緒であるにもかかわらず、なんとなく新しいオブジェクトが作られる印象を与えるからだ。こういったコードはdeceivingであるように思う。

    • DIによって提供される共通のパラメーターを入れたい場合はある。そういうときはBuilderFactoryを作ってしまう(BuilderProviderではない)。このとき、DIによって提供されるオブジェクトはコンストラクタへ、それ以外の必須パラメーターはbuildメソッドに持たせている。

  • Builderをテストするときに、Buildする対象のオブジェクトにどういったパラメーターが渡っているか確認したい。しかし、対象のオブジェクトがそういったパラメーターを公開していない場合がある。このときはBuilderとは別にFactoryを書いて、それをMockするようにする。

こういうことをするとFoo, FooBuilder, FooBuilderFactory, FooFactoryが並ぶことになるが、自分は正しいことをしているのだと固く信じてコードレビューを出す。逆にここらへん妥協したらレビュワーからFactoryBuilderFactory書こうと言われた。

Factory

コンストラクタの抽象化。

fooFactory.create(new A(), new B())

これを使うのはコンストラクタをMockしたいとか作成されるオブジェクトの種類を制御したい(Fooが基底クラス)とかの場合だと思われる。そのために、Factoryのインスタンスを入れ替えたり、Factoryの内部でつくるオブジェクトを制御したりする。

  • Factory自体は変化しない。なんかFactoryに対してなんか設定するのであれば、Factoryのコンストラクト時に完了しているべき。
  • Factoryは常に新しいオブジェクトをつくる。オブジェクトを生成せずに常に同じオブジェクトを返すのであれば、FactoryではなくProviderのほうがよい。(Providerのほうがよりジェネリックな概念)
  • createっぽいメソッドを複数持っても良い。createFromFooみたいなのを持つと便利だったりする。

auto/factoryつかうと勝手に生成されて便利っぽい

@AutoFactory(extends=SomeClass.Factory)
class SomeClass {
  SomeClass(@Provided Something something, String s) {
    ...
  }
  public abstract static class Factory {
    public abstract SomeClass createInner(String s);
    public SomeClass createWithFoo(Foo foo) {
      return createInner(foo.toString());
    }
  }
}

しかしauto/factoryが生成するFactoryはなぜかfinalなので、Mockしようとすると都合がよくない。適当に上のコードみたいにinterfaceとかつくって、それをInjectするようにしたほうが、Mockオブジェクトを注入できて便利になる。しかし、つくったinterfaceと実際のFactoryの結びつきを明示的に書かないといけないというデメリットがある。背に腹は代えられない。

class SomeModule {
  @Provider
  SomeClass.Factory providesSomeClassFactory(SomeClassFactory someClassFactory) {
    return someClassFactory;
  }
}

DaggerにはProviderを継承したFactoryというinterfaceがあるのだが、つかったことはない。コメントを見る限りFactoryはgetのときに常に新しいオブジェクトを生成するという意味を持っているということになっているので、@Injectをコンストラクタに持つようなクラスに対して自動生成されるProviderはFactoryになっていて、それらはFactoryみたいな感じでInjectできるのかもしれない。試していない。

classにfinalつけますか問題

finalの話が出たのでついでにclassにfinalつけますかという話をすると、これは場合によるのかなと思っている。たとえば自分がFramework Authorだった場合、finalを付けたくなる。というのも、フレームワークを使うユーザーに意図しない継承をして欲しくないからだ。意図しない継承をされると、フレームワークの変更をするときになんか内部実装に依存してる感じのコードになったりして困ったりするので、そういうことが起こらないようにfinalにしたいと思う。

逆にフレームワークをつかう側のアプリ開発者の場合、finalにしたくないと思う。アプリ開発者の場合は自分の書いたコードが他の人に再利用される可能性は低いので、下手にfinalにするよりもMockできるようにしておきたいという思惑がある。

どっちもどっちで自分は納得できるので、場合によって切り替えたり、Framework Authorのほうがインターフェースをつくって実装するクラスの方はfinalにしたり、テスト用のオブジェクトを提供したりするのが良さそうだと思っている。

Steve Yeggeもclassにfinalつけますか問題をネタにしている。

Provider

どっか知らないところからオブジェクトを持ってくるためのインターフェース。FactoryはProviderのサブクラス。Providerはほんとにオブジェクトを適当に返すということぐらいしか定義してなくて、そのオブジェクトが呼び出される度に生成される(Factory)とか、常に同じオブジェクトを返すとか、いつ生成されるとか、Scopeローカルとか、そういったことは全く定義されていないので、適当にProviderを使うと変なところでオブジェクトが共有されていたりして死ぬ。こういうことがあるので、ProviderをInjectするのはあんまり推奨されないんじゃないかなぁと思う。(Injectしてなくてもオブジェクトがいつの間にか共有されている問題は解決してはないのだけど)

基本的にJavaのオブジェクト生成は超絶速いということになっているので、共有されるべきでないオブジェクトは無難に毎回生成するのがよさそう。あと共有しようとするとThread-safeにするために、Mutexとか入って更にコストがかかる。Value Objectとかは、下手に共有してMutexのコストをかけるよりもそのまま毎回生成して返すのがスッキリしていて良い。

追記: 中の人によると、ProviderはObjectGraph.get()によって返ってくるオブジェクトを返すという意味になっているらしい。ホントかな。 cf. java - When should I use Factory instead of Provider - Stack Overflow


photo credit: julianrod via photopin cc