オブジェクトグラフ生成フェーズとアプリケーションロジック実行フェーズを分けるとテストしやすくなる?
Googleでアジャイルコーチとして働いているMiško Hevery氏のブログを、このところ興味深く読んでいます。テストしやすいコードの書き方についての記事が多く、とても参考になるのです。
昨夏に公開された「How to Think About the “new” Operator with Respect to Unit Testing」と題する記事では、オブジェクトグラフの生成フェーズとアプリケーションロジックの実行フェーズを分けるべし、と説かれています。オブジェクトグラフとは、私の理解では、どのオブジェクトがどのオブジェクトに依存するかを表すツリー構造のようなもので、DIの利用が前提となっています。Miško氏の主張は、これらのフェーズを分けることでユニットテストしやすいコードが書けるはず、というものです。
言葉では分かりにくいので、Miško氏の主張が最も単純に表現されたコードの例を見てみましょう。
オブジェクトグラフ生成フェーズとアプリケーションロジック実行フェーズが分かれているコード例
public static void main(String[] args) throws Exception { // Creation Phase Server server = new ServerFactory(args).createServer(); // Run Phase server.start(); }
このコードは「My main() Method Is Better Than Yours」と題された記事から引用しました。この記事のタイトルは「俺の main() は君のより良いんだぜ」と挑戦的で楽しいですね。
コードをご覧の通り、オブジェクトグラフの生成フェーズ(Creation Phase)とアプリケーションロジックの実行フェーズ(Run Phase)が見事に分けられています。
なぜフェーズを分けるのか(2009-01-20追記)
なぜこれらのフェーズを分けるとテストしやすくなるのかは「How to Think About the “new” Operator with Respect to Unit Testing」に書かれているとおりです。私なりにまとめると:
class House { private final Kitchen kitchen = new Kitchen(); private boolean isLocked; private boolean isLocked() { return isLocked; } private boolean lock() { kitchen.lock(); isLocked = true; } }
では House が Kitchen に依存してしまい、ユニットテストしづらい。そこで Kitchen を外部から注入して:
class House { private final Kitchen kitchen; private boolean isLocked; public House(Kitchen kitchen) { this.kitchen = kitchen; } private boolean isLocked() { return isLocked; } private boolean lock() { kitchen.lock(); isLocked = true; } }
とすれば、スタブなりモックなりを使ってユニットテストしやすくなる、ということですね。このように、アプリケーションロジックからオブジェクトグラフの生成ロジックを分離するとユニットテストしやすくなります。
でも待てよ
私も大いに納得し、真似をしてコードを書こうと試みたのですが、結果、2つの疑問が浮かんできました。
- argsをなぜコンストラクタに渡さなければならないのか
- オブジェクトグラフの生成に際してargsの解釈(パース)が必要になることもあるのではないか
argsをなぜコンストラクタに渡さなければならないのか
1つめの疑問は、オブジェクトグラフの生成を実際に行うのは createServer() メソッドなので、これにargsを渡せば良いのではないか、というものです。インスタンス変数にする意図が分かりません。複数のServerFactoryインスタンスを作るのであれば分かりますが、その必要はないでしょうし。
というわけで、今のところは createServer() メソッドにargsを渡すのが適切ではないかと考えています。
public static void main(String[] args) throws Exception { // Creation Phase Server server = new ServerFactory().createServer(args); // Run Phase server.start(); }
オブジェクトグラフの生成に際してargsの解釈(パース)が必要になることもあるのではないか
2つめの疑問は、argsをパースしないとオブジェクトグラフが生成できない場合もあるのではないか、そのパースはどこで行えば良いのか、というものです。
あれこれ考えた結果、たとえばコマンドライン引数でログレベルを制御したい場合には、下記のようなコードが適切ではないかと思うに至りました。
public class ServerFactory { public Server createServer(String[] args) { // Parsing Phase Parser parser = new Parser(); parser.parse(args); LogLevel logLevel = parser.getLogLevel(); // Creation Phase Server server = new Server(); server.setLogger(new Logger(logLevel)); return server; } }
つまり、オブジェクトグラフ生成フェーズ(Creation Phase)である createServer() を細かく見れば、コマンドライン引数解釈フェーズ(Parsing Phase)とオブジェクトグラフ生成フェーズに分けられるのではないか、ということです。
引数がログレベルだけであれば、大仰なフェーズ分けはいらないかもしれません。が、ポート番号やらアプリケーションディレクトリやらタイムアウト秒数やらを渡すことを想定すると、JavaならばCommons CLI や Args4j、RubyならばOptionParserによるパース処理がオブジェクトグラフ生成の前提となり、結果的にフェーズ分けが必要になるのではないでしょうか。
コメントしてみたけれど
と、このような疑問が浮かんだので、Miško氏のブログにコメントしてみました。つたない英語しか書けないので意図が通じないかもしれませんし、多忙のGooglerが回答してくれるかどうかも微妙ですが、当たって砕けろの心意気です。コメント欄にmiiの似顔絵が大きく出て驚きました。
追記(2009-01-19)
ありがたいことに回答をいただけました。Guiceモジュールを書くべしという話ですね。
Guiceを使う場合のコード例も「My main() Method Is Better Than Yours」には記されています。
public static void main(String[] args) throws Exception { // Creation Phase Injector injector = Guice.createInjector(new CalculatorServerModule(args)); Server server = injector.getInstance(Server.class); // Run Phase server.start(); }
たぶん、手動DIによるコード例では、Guiceの場合に似せて ServerFactory のコンストラクタにargsを渡しているのでしょう。深い意味はなさそうです。1つめの疑問は解決、ということにしましょう。
2つめの疑問については、DIフレームワークがコマンドライン引数のパースまでしてくれるのであれば、確かに Parsing Phase を意識する必要はなくなります。RubyのDIフレームワークでそこまでしてくれるものがあるのかどうか知りませんが、もしなければ、自作するなり別途 Parsing Phase を用意する必要があるということですね。当面は後者を考えていますが、汎用的にしたくなってDIフレームワーク作りに手を染める未来がすぐそこにある気がします。