望ましいライブラリインターフェースとは
前回の記事で「次回は、Ruptaを作成・公開するにあたって工夫した点や苦労した点について書こうと思っています」と予告しました。予告はしたものの、どこから書こうか迷っていたら、恰好のコメントをid:takahashimさんよりいただいたので、その辺りについて書いてみます。
高橋さんのコメント
特に必要ないならFactoryは使わないでRupta.newかRupta.createみたいなメソッドにした方がいいような
はてなブックマーク - takahashimのブックマーク
コメントありがとうございます。高橋さんにコメントをいただけただけでも公開した甲斐があったと思っています。
Factoryにした理由
さて、現在のRuptaの実装では、Ruptaインスタンスを生成するコードは下記のようになります。
require 'rupta/factory' rupta = Rupta::Factory.new.create
Factoryにした理由をざっくりいうと、Ruptaにコラボレータが存在するからです。
もしFactoryにしていなければ、クライアントコードは下記のようにRuptaインスタンスを生成しなければなりません。
require 'rupta' require 'rupta/uri_purifier' require 'rupta/uri_extractor' require 'rupta/uri_extract_processor/default' require 'rupta/yaml_loader' rupta = Rupta.new( Rupta::UriPurifier.new, Rupta::UriExtractor.new(Rupta::UriExtractProcessor::Default.new), Rupta::YamlLoader.new )
Rupta以外に4つのオブジェクト(コラボレータ)を生成し、手動インジェクトしなければならないわけです。クライアントがこれらのオブジェクトをモックすることはまずないでしょうから、やはりRuptaがFactoryメソッドを提供するのが適切ではないかと考えます。
オブジェクトグラフの生成はFactoryの責務
別の観点から、そもそもRupta程度の小さなライブラリでDIを使う必要はないのではないか、という批判がありえると思います。Ruptaのコンストラクタでnewすればいいじゃないかという見方です。コードにするとこうなります。
class Rupta def initialize(uri_extract_processor = Rupta::UriExtractProcessor::Default.new) @uri_purifier = Rupta::UriPurifier.new @uri_extractor = Rupta::UriExtractor.new(uri_extract_processor) @yaml_loader = Rupta::YamlLoader.new end end
が、やはり私は、オブジェクトグラフの生成はFactoryの責務であると考えます。この辺りは、テスタビリティに関して有用な記事を連発しているGoogler・Miško Hevery氏の影響があります。Factoryの責務については「How to Think About the “new” Operator with Respect to Unit Testing」が分かりやすいと思います。
staticメソッドはテスタビリティを損なう
createメソッドをstaticにしなかった理由もまた、Miško Hevery氏の影響によります。「Static Methods are Death to Testability」と題した記事で氏は、staticメソッドがいかにテスタビリティを損なうか論じています。
仮に、staticなRupta::Factory.createを提供したとしましょう。するとクライアントコードの作成者は、Rupta::Factory.createのラッパーを作らない限り、Rupta::Factoryに依存したコードを書くほかありません。Factoryメソッドがstaticなせいで、クライアントコードのテスタビリティが損なわれてしまうわけです。
もちろんRubyの場合、$LOAD_PATH を変更したり、モックライブラリを使ったり、メソッド自体を書き替えたりなどの方法で、staticメソッドをモックできます。ただ私は、単純なモックオブジェクトをインジェクトできるようにしておくほうが親切ではないかと考えます。モック作成の手段が増えるからです。
大げさなタイトルですみません
「望ましいライブラリインターフェースとは」などと大げさなタイトルにしてしまいましたが、上記のような考え方は、他のライブラリにも適用できるのではないかと思っています。ただ、これが本当に望ましいインターフェースなのかどうか、今のところ自信はありません。もっと様々なライブラリを作っていかないと正解にたどりつけない気がします。異論や疑問などがありましたら、コメントいただければ幸いです。
追記(2009-06-23)
高橋さんよりいただいたコメントについて考えてみたのですが、頭から煙が出そうな感じになってしまったので、現時点での結論めいたことを書きます。
- オプショナル引数だと、オブジェクトグラフが複雑になった場合にメンテが厳しそう
- オブジェクトグラフ生成の責務は、やはりFactoryに負わせたい
- とはいえ、Rupta::Factory.new.create では使いにくいというご指摘はごもっとも
そんなわけで、下記のようなインターフェースが落としどころかなあと考えています。
- ライブラリにコラボレータが存在しない場合
- Library.new
- ライブラリにはコラボレータが存在するが、Factoryにはコラボレータが存在しない場合
- Library::Factory.create(library_collaborators)
- ライブラリにもFactoryにもコラボレータが存在する場合
- Library::Factory.new(factory_collaborators).create(library_collaborators)
Factoryのコラボレータとして想定しているのは、ARGVのパーサとか定義ファイルのローダとか「それがないとライブラリのオブジェクトグラフが作れないオブジェクト」です。
追記(2009-06-25)
そんなわけで、Factoryをstaticメソッドにしました。
考えてみれば、Factoryのコラボレータをクライアントから渡すなんてことはなさそうです。Library.new か Library::Factory.create(library_collaborators) にすべしというのが当面の結論です。