Javadocを書く

最近は時間を作ってEffective Javaの2版をよんでます。

Effective Java (Java Series)

Effective Java (Java Series)

ほとんど1版と同じ内容ですが、"Item 44: Write doc comments for all exposed API elements" を読んでよくまとまってるなと思ったので、触発されてメモがてらに私のやり方を。

引用の2段落目は基本的に超約。

どこに書くか

If an API is to be usable, it must be documented.

ユーザが利用可能なすべてのAPIJavadocを書く。

これはとりあえず必須だと思います。ちなみに私はこの辺には必ず入れるように心がけてます。

  • public な型、フィールド、コンストラクタ、メソッド
  • protected なフィールド、メソッド
  • package private なメソッドのうち、テストされるべきもの

なにを書くか

The doc comment for a method should describe succinctly the contract between the method and its client.

メソッドのJavadocには、メソッドと利用者間の契約を簡潔に書く。

契約、のいい表現が思いつきませんでしたが、私はチェックリスト的なものに答えていく形でJavadoc書いてます。

  1. このメソッドは何をしますか? (what)
  2. このメソッドは(渡した引数/オブジェクトの状態/環境の状態に対して)何を返しますか? (invariant, postcondition)
    1. 返される値にはどのような特徴がありますか?
    2. 返された値をどう使えばいいですか?
  3. このメソッドには、どんな引数を渡せばいいですか? (precondition)
    1. そこに書いていない値が渡された場合、何が起こりますか?
  4. このメソッドを呼び出す前に、何をすればいいですか、何をしてはいけないですか? (precondition)
    1. その条件を満たしていない場合、何が起こりますか?
  5. このメソッドを呼び出すと、どんな影響がありますか? (side effect)
  6. (ここまでに書いたすべての動詞に対して) その動作が失敗することはありますか?
    1. 失敗した場合、どのような影響がありますか?

大抵のメソッドの正常系は空気を読めば動きがわかるのですが、異常系は設計者のWhyまで汲み取らないと基本的に判りません。

/**
 * このオブジェクトに格納されたデータのうち、指定のプレフィックスを持つものを返す。
 * @param prefix プレフィックス
 * @return 指定のプレフィックスを持つもの
 */
String[] find(String prefix);

といわれても、見つからなかったときにnullが返ってくるのか長さ0のString配列なのか、java.util.NoSuchElementExceptionがスローされるのかいまいち不明です。
見つかった要素の個数を調べたいだけなのに、わざわざこんなコード書かなきゃならなくなります。

try {
  String[] results = find("com.example.");
  if (results == null) {
    return 0;
  }
  return results.length;
}
catch (NoSuchElementException e) {
  return 0;
}

この程度だったらソースコード読むか、実行してみて経験的に〜という流れかもしれませんがどちらにしろスピード感が失われます。

もし、チェックリストと照らし合わせて書くならこんな感じでしょうか ([〜]の部分は記載しないですよ)。

/**
 * このオブジェクトに格納されたデータのうち、指定のプレフィックスを持つものを返す。
 * [返される値の特徴]
 *   返される{@link String}の配列に含まれる各要素は、すべて引数に指定された
 *   文字列をその先頭に持ち、辞書式順序に従って整列される。
 * [動作の失敗]
 *   引数に指定された文字列をプレフィックスにもつデータがこのオブジェクトに存在しない場合、
 *   この呼び出しは長さ0の配列を返す。
 * [引数の説明にない値]
 *   {@code prefix}に空の文字列が渡された場合、
 *   返される配列にはオブジェクトに格納されたすべてのデータが含まれる。
 * @param prefix
 *     プレフィックス、または空の文字列
 * @return
 *     このオブジェクトに格納されたデータのうち、指定のプレフィックスを持つものの一覧。
 *     引数に空の文字列が指定された場合は、このオブジェクトに格納されたすべてのデータ
 * @throws IllegalArgumentException
 *     [引数の説明にない値]
 *       引数に{@code null}が指定された場合
 */
String[] find(String prefix);

普段はもうちょっと流暢になるように校正しますが、大体こんな感じです。ここまで書ければ大抵の単体テストケースは浮かび上がりますし、実装もほとんど頭の中に出来上がってる状態になります。

よく「getter/setterのJavadocは無駄」ということを聞きますが、チェックリストに当てはめてみると意外と書かなきゃならないことがあったりします。

/**
 * このオブジェクトに付与された属性の一覧を返す。
 * [オブジェクトの状態]
 *   このオブジェクトに属性がひとつも設定されていない場合、
 *   この呼び出しは要素数0の{@link Set}を返す。
 * [どう使うか]
 *   返される{@link Set}を直接操作することで、
 *   このオブジェクトの属性を変更することができる。
 *   ただしこの操作はスレッドアンセーフなので、
 *   複数のスレッドからこのオブジェクトを利用する場合、適切に同期されていなければならない。
 * @return このオブジェクトに付与された属性の一覧
 * @throws IllegalStateException
 *     [precondition]
 *       このオブジェクトに対してすでに{@link #close()}が呼び出され、利用不可能になっている場合
 */
Set<String> getAttributes();

といった具合です。

とはいっても上記のチェックリストを全部ちゃんとやるのはめんどくせーと自分でもよく思うのですが、私はそこで逆にJavadocが書きやすいようにインターフェースを設計しなおしてますJavadocに書いたことは単体テストもするという自分ルールがあるので、100行もあるようなJavadocを書いたらそれだけで単体テストが50項目くらい増えます。

Javadoc原理主義

何で私がそんなにJavadocにこだわるかといえば、

  • Javadocに書けないようなプログラムはどうせうまく動かないし、人に説明できない
    • 実装始める前に、Javadocに動作を規定できるか試してみる
  • 実プログラムは抽象度が低すぎて網羅性の検証が難しい
  • 実装しかない状態で単体テスト書くと、実装に配慮したテストになる
    • 「意図する動作」と「実装」の乖離を検出するためにテスト書くのに、「実装」から「意図する動作」を逆算すれば、そりゃね

という考えがあるからです。

下の人はそんなにこだわらなくてもいいのかも。

  • 常に動作が説明できる
  • 実装しながらパス網羅できる
  • 意図した動作が最初から最後までぶれない
  • 別に詳細設計書作っている

ちなみに現在、メソッド呼び出したときに@throwsに書いてないランタイム例外が飛んでくると残念な気持ちになる程度のステージです。

で、

長々と書いてしまいましたが、(ここまでたどり着いた人がいるなら)Javadoc書くときにやってるプラクティスとか教えてくださいまし。逆にJavadocいらねーよって言う意見も。