イリュージョニストにならないために

前回は「クライアントにとって使いやすいAPI」について語りました。今回は「読みやすい実装」について。ネタ元は同じくSqlExecutor。

まず。javadoc厨で契約(仕様)原理主義の立場でいきなり厳しいことを言ってしまえば、「そもそもクライアントに実装を読ますなよ。javadocで全部伝わるように書け。」ってことになるのだが、まぁまぁ、そこまでスパルタンにはなるまい。

でだ。何か追いかけたい事があって、メソッドの実装を追いかけ始めた。そして、以下のようなコードに出会ったとする。

java.sql.Statement stmt = ...;
java.sql.ResultSet rs = ...;
ResultHandler handler = ...;
String sql = ...;

if (stmt.execute(sql)) {
  rs = stmt.getResultSet();
  handler.handleResultSet(sql, rs);
} else {
  int count = stmt.getUpdateCount();
  if (count >= 0) {
    handler.handleUpdateCount(sql, count);
  }
}

さてここで。「executeを実行したら、それに対して必ずhandlerが呼ばれるだろうか?」という視点でこれを見てもらいたい。
必ず呼ばれると思った方。素晴らしいですね。Statementの仕様をよく理解されています。「理解してたんじゃなくて調べたんだよ」という場合でも、素晴らしい。ここで仕様を調べにかかる、という姿勢が素晴らしい。

必ずしも呼ばれないかもしれないと思った方。恐らく、ごく一般的です。恐らく「executeの結果がfalseで、かつcountが-1だったら handlerが呼ばれないことがあるか…ふーん…」と思ったのだと思います。あなたはイリュージョンを見たのですね。

解説

種明かしをしてしまうと、Statement#execute()がfalseを返した場合は、Statement#getUpdateCount()は必ず0以上の整数を返します。executeがtrueを返した時は-1を返します。つまり上記のコードで、count >= 0 がfalseとなることはありえません。つまり、handlerは必ず呼ばれるんです。

execute getResultSet getUpdateCount
true != null == -1
false == null >= 0

しかし、この事は「Statementの仕様を把握しないと」見えてきません。上記の通り「handlerが呼ばれない経路、あるはずのないものが見えてしまう人」もいるのです、っていうかそういう人の方が多いんです(多分)。

また、executeがtrueの場合とfalseの場合の対称性が確保されていません。falseの時getUpdateCountが0以上であることをチェックするのであれば、trueの時にgetResultSetのnullをチェックすべきです。これも誤解を引き起こす原因のひとつ。

このような「イリュージョンを見せてしまうコード」は読みやすいとは言えません。

対応1
if (stmt.execute(sql)) {
  rs = stmt.getResultSet();
  if (rs == null) {
    throw new Error("java.sql.Statementの挙動が仕様通りではない、つまりJava実装のバグ。通常は発生しえない。");
  }
  handler.handleResultSet(sql, rs);
} else {
  int count = stmt.getUpdateCount();
  if (count < 0) {
    throw new Error("java.sql.Statementの挙動が仕様通りではない、つまりJava実装のバグ。通常は発生しえない。");
  }
  handler.handleUpdateCount(sql, count);
}

Statementが仕様と異なる動きをしたら、Errorをぶん投げます。まぁ、ここで例外じゃなくてErrorを選択したのは私のポリシーより。ここは論点じゃないので、別のポリシーを持っていたらRuntimeException系になるかもしれないです。

要は「あり得ないことが起きたのならば、クライアントに通知する」のが大事だと思った場合に取る選択肢。

対応2
if (stmt.execute(sql)) {
  rs = stmt.getResultSet();
  handler.handleResultSet(sql, rs);
} else {
  int count = stmt.getUpdateCount();
  handler.handleUpdateCount(sql, count);
}

ありえないこととして、そもそもチェックしないという選択肢。Java実装のバグをいちいち疑ってたら、エラいことになってしまうでしょう…。


というわけで、このような対応をすれば、イリュージョンを見せられることもなくなるでしょう。下手にクライアントを惑わさないこと。コレ大事。

// getMoreResultsについて考えなきゃなぁ。